44
55namespace OpenForgeProject \MageForge \Console \Command \Hyva ;
66
7+ use Laravel \Prompts \MultiSelectPrompt ;
78use Magento \Framework \Console \Cli ;
89use OpenForgeProject \MageForge \Console \Command \AbstractCommand ;
910use 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