refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives

This commit is contained in:
2025-11-04 13:44:27 +01:00
parent 980714f656
commit bfce93ce77
110 changed files with 2828 additions and 774 deletions

View 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);
}
}
}