From 83f43e7b6205a0d7599e0ef4ff4dd02f0deff55a Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 10 Aug 2026 15:18:28 +0200 Subject: [PATCH 1/2] feat: add interactive menu for managing MageForge Inspector actions --- src/Console/Command/Dev/InspectorCommand.php | 53 ++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/Console/Command/Dev/InspectorCommand.php b/src/Console/Command/Dev/InspectorCommand.php index cbc36359..c5c59411 100644 --- a/src/Console/Command/Dev/InspectorCommand.php +++ b/src/Console/Command/Dev/InspectorCommand.php @@ -4,6 +4,7 @@ namespace OpenForgeProject\MageForge\Console\Command\Dev; +use Laravel\Prompts\SelectPrompt; use Magento\Framework\App\Cache\Manager as CacheManager; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Config\Storage\WriterInterface; @@ -51,12 +52,15 @@ protected function configure(): void ->setDescription('Manage MageForge Frontend Inspector (Actions: enable|disable|status)') ->addArgument( self::ARGUMENT_ACTION, - InputArgument::REQUIRED, - 'Action to perform: enable, disable, or status', + InputArgument::OPTIONAL, + 'Action to perform: enable, disable, or status (interactive menu if omitted)', ) ->setHelp(<<%command.name% command manages the MageForge Frontend Inspector: + php %command.full_name% + Show an interactive menu to enable or disable the inspector + php %command.full_name% enable Enable the inspector (requires developer mode) @@ -83,7 +87,20 @@ protected function configure(): void protected function executeCommand(InputInterface $input, OutputInterface $output): int { $arg = $input->getArgument(self::ARGUMENT_ACTION); - $action = strtolower(is_string($arg) ? $arg : ''); + $action = strtolower(is_string($arg) ? trim($arg) : ''); + + // No action given: show interactive menu, fall back to status in non-interactive mode + if ($action === '') { + if (!$this->isInteractiveTerminal($output)) { + return $this->showStatus(); + } + + $selectedAction = $this->promptAction(); + if ($selectedAction === null) { + return Cli::RETURN_FAILURE; + } + $action = $selectedAction; + } // Validate action if (!in_array($action, ['enable', 'disable', 'status'], true)) { @@ -110,6 +127,36 @@ protected function executeCommand(InputInterface $input, OutputInterface $output }; } + /** + * Prompt user to select an action via interactive menu + * + * @return string|null The selected action (enable, disable, status), or null if cancelled/failed + */ + private function promptAction(): ?string + { + $currentStatus = $this->isInspectorEnabled() ? 'enabled' : 'disabled'; + + $prompt = new SelectPrompt( + label: sprintf('MageForge Inspector is currently %s – select an action', $currentStatus), + options: [ + 'enable' => 'Enable inspector', + 'disable' => 'Disable inspector', + 'status' => 'Show status', + ], + default: $this->isInspectorEnabled() ? 'disable' : 'enable', + hint: 'Arrow keys to navigate, Enter to confirm', + ); + + try { + $selection = $prompt->prompt(); + \Laravel\Prompts\Prompt::terminal()->restoreTty(); + return is_string($selection) ? $selection : null; + } catch (\Exception $e) { + $this->io->error('Selection failed: ' . $e->getMessage()); + return null; + } + } + /** * Enable inspector * From 91c77e96a65fa32c03f1a82aeb57f50993c74729 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 10 Aug 2026 17:02:46 +0200 Subject: [PATCH 2/2] fix: Refactor promptAction method visibility and enhance interactive command tests --- src/Console/Command/Dev/InspectorCommand.php | 9 ++- .../Command/Dev/InspectorCommandTest.php | 80 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/Console/Command/Dev/InspectorCommand.php b/src/Console/Command/Dev/InspectorCommand.php index c5c59411..271b489b 100644 --- a/src/Console/Command/Dev/InspectorCommand.php +++ b/src/Console/Command/Dev/InspectorCommand.php @@ -132,7 +132,7 @@ protected function executeCommand(InputInterface $input, OutputInterface $output * * @return string|null The selected action (enable, disable, status), or null if cancelled/failed */ - private function promptAction(): ?string + protected function promptAction(): ?string { $currentStatus = $this->isInspectorEnabled() ? 'enabled' : 'disabled'; @@ -147,13 +147,18 @@ private function promptAction(): ?string hint: 'Arrow keys to navigate, Enter to confirm', ); + // Set environment variables for Laravel Prompts (Docker/DDEV compatibility) + $this->setPromptEnvironment(); + try { $selection = $prompt->prompt(); - \Laravel\Prompts\Prompt::terminal()->restoreTty(); return is_string($selection) ? $selection : null; } catch (\Exception $e) { $this->io->error('Selection failed: ' . $e->getMessage()); return null; + } finally { + \Laravel\Prompts\Prompt::terminal()->restoreTty(); + $this->resetPromptEnvironment(); } } diff --git a/tests/Unit/Console/Command/Dev/InspectorCommandTest.php b/tests/Unit/Console/Command/Dev/InspectorCommandTest.php index 32c0ae16..367472c1 100644 --- a/tests/Unit/Console/Command/Dev/InspectorCommandTest.php +++ b/tests/Unit/Console/Command/Dev/InspectorCommandTest.php @@ -13,6 +13,7 @@ use OpenForgeProject\MageForge\Model\Config\Inspector as InspectorConfig; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; class InspectorCommandTest extends TestCase @@ -229,4 +230,83 @@ public function testStatusUppercasesActionArgument(): void $this->assertSame(Cli::RETURN_SUCCESS, $exitCode); $this->assertStringContainsString('MageForge Inspector Status', $tester->getDisplay()); } + + // ------------------------------------------------------------------------- + // No-argument execution path (interactive menu / non-interactive fallback) + // ------------------------------------------------------------------------- + + public function testNoActionFallsBackToStatusInNonInteractiveMode(): void + { + $this->state->method('getMode')->willReturn(State::MODE_DEVELOPER); + $this->scopeConfig->method('isSetFlag')->willReturn(true); + $this->configWriter->expects($this->never())->method('save'); + + $tester = new CommandTester($this->command); + $exitCode = $tester->execute([]); + + $this->assertSame(Cli::RETURN_SUCCESS, $exitCode); + $this->assertStringContainsString('MageForge Inspector Status', $tester->getDisplay()); + } + + public function testNoActionUsesSelectedActionFromInteractiveMenu(): void + { + $this->state->method('getMode')->willReturn(State::MODE_DEVELOPER); + $this->configWriter->expects($this->once()) + ->method('save') + ->with(InspectorConfig::XML_PATH_ENABLED, '0'); + + $tester = new CommandTester($this->createInteractiveCommand('disable')); + $exitCode = $tester->execute([]); + + $this->assertSame(Cli::RETURN_SUCCESS, $exitCode); + $this->assertStringContainsString('has been disabled', $tester->getDisplay()); + } + + public function testNoActionFailsWhenInteractiveMenuIsCancelled(): void + { + $this->configWriter->expects($this->never())->method('save'); + + $tester = new CommandTester($this->createInteractiveCommand(null)); + $exitCode = $tester->execute([]); + + $this->assertSame(Cli::RETURN_FAILURE, $exitCode); + } + + /** + * Create a command double that always takes the interactive path and returns + * the given selection from the menu instead of rendering a real prompt + * + * @param string|null $selection + * @return InspectorCommand + */ + private function createInteractiveCommand(?string $selection): InspectorCommand + { + return new class( + $this->configWriter, + $this->state, + $this->cacheManager, + $this->scopeConfig, + $selection, + ) extends InspectorCommand { + public function __construct( + WriterInterface $configWriter, + State $state, + CacheManager $cacheManager, + ScopeConfigInterface $scopeConfig, + private readonly ?string $selection, + ) { + parent::__construct($configWriter, $state, $cacheManager, $scopeConfig); + } + + protected function isInteractiveTerminal(OutputInterface $output): bool + { + return true; + } + + protected function promptAction(): ?string + { + return $this->selection; + } + }; + } }