Skip to content

Commit c959f74

Browse files
committed
Add interactive menu with Laravel Prompts
- Interactive mode activated when no options provided - Multi-select menu for scan options: * Show all modules * Include Magento core modules * Detailed file-level issues - Displays selected configuration before scan - Falls back to direct mode if interactive fails - Consistent with theme:build command UX
1 parent 44d98e1 commit c959f74

1 file changed

Lines changed: 230 additions & 7 deletions

File tree

src/Console/Command/Hyva/CompatibilityCheckCommand.php

Lines changed: 230 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace OpenForgeProject\MageForge\Console\Command\Hyva;
66

7+
use Laravel\Prompts\MultiSelectPrompt;
78
use Magento\Framework\Console\Cli;
89
use OpenForgeProject\MageForge\Console\Command\AbstractCommand;
910
use OpenForgeProject\MageForge\Service\Hyva\CompatibilityChecker;
@@ -19,6 +20,9 @@ class CompatibilityCheckCommand extends AbstractCommand
1920
private const OPTION_INCLUDE_VENDOR = 'include-vendor';
2021
private const OPTION_DETAILED = 'detailed';
2122

23+
private array $originalEnv = [];
24+
private array $secureEnvStorage = [];
25+
2226
public function __construct(
2327
private readonly CompatibilityChecker $compatibilityChecker
2428
) {
@@ -57,6 +61,85 @@ protected function configure(): void
5761
}
5862

5963
protected function executeCommand(InputInterface $input, OutputInterface $output): int
64+
{
65+
// Check if we're in interactive mode (no options provided)
66+
$hasOptions = $input->getOption(self::OPTION_SHOW_ALL)
67+
|| $input->getOption(self::OPTION_THIRD_PARTY_ONLY)
68+
|| $input->getOption(self::OPTION_INCLUDE_VENDOR)
69+
|| $input->getOption(self::OPTION_DETAILED);
70+
71+
if (!$hasOptions && $this->isInteractiveTerminal($output)) {
72+
return $this->runInteractiveMode($input, $output);
73+
}
74+
75+
return $this->runDirectMode($input, $output);
76+
}
77+
78+
/**
79+
* Run interactive mode with Laravel Prompts
80+
*/
81+
private function runInteractiveMode(InputInterface $input, OutputInterface $output): int
82+
{
83+
$this->io->title('Hyvä Theme Compatibility Check');
84+
85+
// Set environment variables for Laravel Prompts
86+
$this->setPromptEnvironment();
87+
88+
try {
89+
$scanOptionsPrompt = new MultiSelectPrompt(
90+
label: 'Select scan options',
91+
options: [
92+
'show-all' => 'Show all modules including compatible ones',
93+
'include-vendor' => 'Include Magento core modules (default: third-party only)',
94+
'detailed' => 'Show detailed file-level issues with line numbers',
95+
],
96+
default: [],
97+
hint: 'Space to toggle, Enter to confirm. Default: third-party modules only',
98+
required: false,
99+
);
100+
101+
$selectedOptions = $scanOptionsPrompt->prompt();
102+
\Laravel\Prompts\Prompt::terminal()->restoreTty();
103+
$this->resetPromptEnvironment();
104+
105+
// Apply selected options to input
106+
$showAll = in_array('show-all', $selectedOptions);
107+
$includeVendor = in_array('include-vendor', $selectedOptions);
108+
$detailed = in_array('detailed', $selectedOptions);
109+
$thirdPartyOnly = false; // Not needed in interactive mode
110+
111+
// Show selected configuration
112+
$this->io->newLine();
113+
$config = [];
114+
if ($showAll) {
115+
$config[] = 'Show all modules';
116+
}
117+
if ($includeVendor) {
118+
$config[] = 'Include Magento core';
119+
} else {
120+
$config[] = 'Third-party modules only';
121+
}
122+
if ($detailed) {
123+
$config[] = 'Detailed issues';
124+
}
125+
$this->io->comment('Configuration: ' . implode(', ', $config));
126+
$this->io->newLine();
127+
128+
// Run scan with selected options
129+
return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, $output);
130+
} catch (\Exception $e) {
131+
$this->resetPromptEnvironment();
132+
$this->io->error('Interactive mode failed: ' . $e->getMessage());
133+
$this->io->info('Falling back to default scan (third-party modules only)...');
134+
$this->io->newLine();
135+
return $this->runDirectMode($input, $output);
136+
}
137+
}
138+
139+
/**
140+
* Run direct mode with command line options
141+
*/
142+
private function runDirectMode(InputInterface $input, OutputInterface $output): int
60143
{
61144
$showAll = $input->getOption(self::OPTION_SHOW_ALL);
62145
$thirdPartyOnly = $input->getOption(self::OPTION_THIRD_PARTY_ONLY);
@@ -65,6 +148,20 @@ protected function executeCommand(InputInterface $input, OutputInterface $output
65148

66149
$this->io->title('Hyvä Theme Compatibility Check');
67150

151+
return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, $output);
152+
}
153+
154+
/**
155+
* Run the actual compatibility scan
156+
*/
157+
private function runScan(
158+
bool $showAll,
159+
bool $thirdPartyOnly,
160+
bool $includeVendor,
161+
bool $detailed,
162+
OutputInterface $output
163+
): int {
164+
68165
// Determine filter logic:
69166
// - Default (no flags): Scan third-party only (exclude Magento_* but include vendor third-party)
70167
// - With --include-vendor: Scan everything including Magento_*
@@ -92,6 +189,11 @@ protected function executeCommand(InputInterface $input, OutputInterface $output
92189
// Display summary
93190
$this->displaySummary($results);
94191

192+
// Display recommendations if there are issues
193+
if ($results['hasIncompatibilities']) {
194+
$this->displayRecommendations();
195+
}
196+
95197
// Return appropriate exit code
96198
return $results['summary']['criticalIssues'] > 0
97199
? Cli::RETURN_FAILURE
@@ -192,19 +294,13 @@ private function displaySummary(array $results): void
192294
} else {
193295
$this->io->success('All scanned modules are Hyvä compatible!');
194296
}
195-
196-
// Provide helpful recommendations
197-
$this->displayRecommendations($results);
198297
}
199298

200299
/**
201300
* Display helpful recommendations
202301
*/
203-
private function displayRecommendations(array $results): void
302+
private function displayRecommendations(): void
204303
{
205-
if ($results['summary']['incompatible'] === 0) {
206-
return;
207-
}
208304

209305
$this->io->section('Recommendations');
210306

@@ -219,4 +315,131 @@ private function displayRecommendations(array $results): void
219315
$this->io->text($recommendation);
220316
}
221317
}
318+
319+
/**
320+
* Check if running in an interactive terminal
321+
*/
322+
private function isInteractiveTerminal(OutputInterface $output): bool
323+
{
324+
// Check if output is decorated (supports ANSI codes)
325+
if (!$output->isDecorated()) {
326+
return false;
327+
}
328+
329+
// Check if STDIN is available and readable
330+
if (!defined('STDIN') || !is_resource(STDIN)) {
331+
return false;
332+
}
333+
334+
// Check for common non-interactive environments
335+
$nonInteractiveEnvs = [
336+
'CI',
337+
'GITHUB_ACTIONS',
338+
'GITLAB_CI',
339+
'JENKINS_URL',
340+
'TEAMCITY_VERSION',
341+
];
342+
343+
foreach ($nonInteractiveEnvs as $env) {
344+
if ($this->getEnvVar($env) || $this->getServerVar($env)) {
345+
return false;
346+
}
347+
}
348+
349+
// Additional check: try to detect if running in a proper TTY
350+
$sttyOutput = shell_exec('stty -g 2>/dev/null');
351+
return !empty($sttyOutput);
352+
}
353+
354+
/**
355+
* Set environment for Laravel Prompts to work properly in Docker/DDEV
356+
*/
357+
private function setPromptEnvironment(): void
358+
{
359+
// Store original values for reset
360+
$this->originalEnv = [
361+
'COLUMNS' => $this->getEnvVar('COLUMNS'),
362+
'LINES' => $this->getEnvVar('LINES'),
363+
'TERM' => $this->getEnvVar('TERM'),
364+
];
365+
366+
// Set terminal environment variables using safe method
367+
$this->setEnvVar('COLUMNS', '100');
368+
$this->setEnvVar('LINES', '40');
369+
$this->setEnvVar('TERM', 'xterm-256color');
370+
}
371+
372+
/**
373+
* Reset terminal environment after prompts
374+
*/
375+
private function resetPromptEnvironment(): void
376+
{
377+
// Reset environment variables to original state using secure methods
378+
foreach ($this->originalEnv as $key => $value) {
379+
if ($value === null) {
380+
// Remove from our secure cache
381+
$this->removeSecureEnvironmentValue($key);
382+
} else {
383+
// Restore original value using secure method
384+
$this->setEnvVar($key, $value);
385+
}
386+
}
387+
}
388+
389+
/**
390+
* Securely remove environment variable from cache
391+
*/
392+
private function removeSecureEnvironmentValue(string $name): void
393+
{
394+
// Remove the specific variable from our secure storage
395+
unset($this->secureEnvStorage[$name]);
396+
397+
// Clear the static cache to force refresh on next access
398+
$this->clearEnvironmentCache();
399+
}
400+
401+
/**
402+
* Simplified environment variable getter
403+
*/
404+
private function getEnvVar(string $name): ?string
405+
{
406+
// Check secure storage first
407+
if (isset($this->secureEnvStorage[$name])) {
408+
return $this->secureEnvStorage[$name];
409+
}
410+
411+
// Fall back to system environment
412+
$value = getenv($name);
413+
return $value !== false ? $value : null;
414+
}
415+
416+
/**
417+
* Simplified server variable getter
418+
*/
419+
private function getServerVar(string $name): ?string
420+
{
421+
return $_SERVER[$name] ?? null;
422+
}
423+
424+
/**
425+
* Simplified environment variable setter
426+
*/
427+
private function setEnvVar(string $name, string $value): void
428+
{
429+
$this->secureEnvStorage[$name] = $value;
430+
putenv("$name=$value");
431+
}
432+
433+
/**
434+
* Clear environment cache
435+
*/
436+
private function clearEnvironmentCache(): void
437+
{
438+
// Force refresh on next access by clearing our storage
439+
$this->secureEnvStorage = array_filter(
440+
$this->secureEnvStorage,
441+
fn($key) => in_array($key, ['COLUMNS', 'LINES', 'TERM']),
442+
ARRAY_FILTER_USE_KEY
443+
);
444+
}
222445
}

0 commit comments

Comments
 (0)