44
55namespace OpenForgeProject \MageForge \Console \Command ;
66
7+ use Laravel \Prompts \SelectPrompt ;
78use Magento \Framework \Console \Cli ;
9+ use OpenForgeProject \MageForge \Service \ThemeSuggester ;
810use Symfony \Component \Console \Command \Command ;
911use Symfony \Component \Console \Input \InputInterface ;
1012use Symfony \Component \Console \Output \OutputInterface ;
@@ -27,6 +29,16 @@ abstract class AbstractCommand extends Command
2729 */
2830 protected SymfonyStyle $ io ;
2931
32+ /**
33+ * @var array
34+ */
35+ private array $ originalEnv = [];
36+
37+ /**
38+ * @var array
39+ */
40+ private array $ secureEnvStorage = [];
41+
3042 /**
3143 * Get the command name with proper group structure
3244 *
@@ -101,4 +113,196 @@ protected function isDebug(OutputInterface $output): bool
101113 {
102114 return $ output ->getVerbosity () >= OutputInterface::VERBOSITY_DEBUG ;
103115 }
116+
117+ /**
118+ * Handle invalid theme with interactive suggestions
119+ *
120+ * When a theme code is invalid, this method finds similar themes using Levenshtein distance
121+ * and offers an interactive selection via Laravel Prompts (if terminal is interactive).
122+ * In non-interactive environments, suggestions are displayed as text.
123+ *
124+ * @param string $invalidTheme The invalid theme code entered by user
125+ * @param ThemeSuggester $themeSuggester Service to find similar themes
126+ * @param OutputInterface $output Output interface for terminal detection
127+ * @return string|null The selected theme code, or null if cancelled/no selection
128+ */
129+ protected function handleInvalidThemeWithSuggestions (
130+ string $ invalidTheme ,
131+ ThemeSuggester $ themeSuggester ,
132+ OutputInterface $ output
133+ ): ?string {
134+ $ suggestions = $ themeSuggester ->findSimilarThemes ($ invalidTheme );
135+
136+ // No suggestions found
137+ if (empty ($ suggestions )) {
138+ $ this ->io ->error ("Theme ' $ invalidTheme' is not installed and no similar themes were found. " );
139+ return null ;
140+ }
141+
142+ // Check if terminal is interactive
143+ if (!$ this ->isInteractiveTerminal ($ output )) {
144+ // Non-interactive fallback: display suggestions as text
145+ $ this ->io ->error ("Theme ' $ invalidTheme' is not installed. " );
146+ $ this ->io ->writeln ("\nDid you mean one of these? " );
147+ foreach ($ suggestions as $ suggestion ) {
148+ $ this ->io ->writeln (" - $ suggestion " );
149+ }
150+ return null ;
151+ }
152+
153+ // Interactive mode: show prompt with suggestions
154+ $ this ->io ->error ("Theme ' $ invalidTheme' is not installed. " );
155+ $ this ->io ->newLine ();
156+
157+ // Prepare options with "None of these" option
158+ $ options = array_merge ($ suggestions , ['None of these ' ]);
159+
160+ // Set environment for Docker/DDEV compatibility
161+ $ this ->setPromptEnvironment ();
162+
163+ $ prompt = new SelectPrompt (
164+ label: 'Did you mean one of these themes? ' ,
165+ options: $ options ,
166+ scroll: 10 ,
167+ hint: 'Arrow keys to navigate, Enter to confirm '
168+ );
169+
170+ try {
171+ $ selection = $ prompt ->prompt ();
172+ \Laravel \Prompts \Prompt::terminal ()->restoreTty ();
173+ $ this ->resetPromptEnvironment ();
174+
175+ // Check if user selected "None of these"
176+ if ($ selection === 'None of these ' ) {
177+ return null ;
178+ }
179+
180+ return $ selection ;
181+ } catch (\Exception $ e ) {
182+ $ this ->resetPromptEnvironment ();
183+ $ this ->io ->error ('Selection failed: ' . $ e ->getMessage ());
184+ return null ;
185+ }
186+ }
187+
188+ /**
189+ * Check if terminal is interactive (supports Laravel Prompts)
190+ *
191+ * @param OutputInterface $output
192+ * @return bool
193+ */
194+ private function isInteractiveTerminal (OutputInterface $ output ): bool
195+ {
196+ // Check if output supports ANSI
197+ if (!$ output ->isDecorated ()) {
198+ return false ;
199+ }
200+
201+ // Check if STDIN is available
202+ if (!defined ('STDIN ' ) || !is_resource (STDIN )) {
203+ return false ;
204+ }
205+
206+ // Check for CI environments
207+ $ nonInteractiveEnvs = [
208+ 'CI ' ,
209+ 'GITHUB_ACTIONS ' ,
210+ 'GITLAB_CI ' ,
211+ 'JENKINS_URL ' ,
212+ 'TEAMCITY_VERSION ' ,
213+ ];
214+
215+ foreach ($ nonInteractiveEnvs as $ env ) {
216+ if ($ this ->getEnvVar ($ env ) || $ this ->getServerVar ($ env )) {
217+ return false ;
218+ }
219+ }
220+
221+ // Check if TTY is available
222+ $ sttyOutput = shell_exec ('stty -g 2>/dev/null ' );
223+ return !empty ($ sttyOutput );
224+ }
225+
226+ /**
227+ * Set environment variables for Laravel Prompts in Docker/DDEV
228+ *
229+ * @return void
230+ */
231+ private function setPromptEnvironment (): void
232+ {
233+ // Store original values for restoration
234+ $ this ->originalEnv = [
235+ 'COLUMNS ' => $ this ->getEnvVar ('COLUMNS ' ),
236+ 'LINES ' => $ this ->getEnvVar ('LINES ' ),
237+ 'TERM ' => $ this ->getEnvVar ('TERM ' ),
238+ ];
239+
240+ // Set terminal dimensions for proper rendering
241+ $ this ->setEnvVar ('COLUMNS ' , '100 ' );
242+ $ this ->setEnvVar ('LINES ' , '40 ' );
243+ $ this ->setEnvVar ('TERM ' , 'xterm-256color ' );
244+ }
245+
246+ /**
247+ * Reset environment variables to original state
248+ *
249+ * @return void
250+ */
251+ private function resetPromptEnvironment (): void
252+ {
253+ foreach ($ this ->originalEnv as $ key => $ value ) {
254+ if ($ value === null ) {
255+ $ this ->removeSecureEnvironmentValue ($ key );
256+ } else {
257+ $ this ->setEnvVar ($ key , $ value );
258+ }
259+ }
260+ }
261+
262+ /**
263+ * Get environment variable value
264+ *
265+ * @param string $key
266+ * @return string|null
267+ */
268+ private function getEnvVar (string $ key ): ?string
269+ {
270+ return getenv ($ key ) ?: null ;
271+ }
272+
273+ /**
274+ * Get server variable value
275+ *
276+ * @param string $key
277+ * @return string|null
278+ */
279+ private function getServerVar (string $ key ): ?string
280+ {
281+ return $ _SERVER [$ key ] ?? null ;
282+ }
283+
284+ /**
285+ * Set environment variable securely
286+ *
287+ * @param string $key
288+ * @param string $value
289+ * @return void
290+ */
291+ private function setEnvVar (string $ key , string $ value ): void
292+ {
293+ $ this ->secureEnvStorage [$ key ] = $ value ;
294+ putenv ("$ key= $ value " );
295+ }
296+
297+ /**
298+ * Remove environment variable securely
299+ *
300+ * @param string $key
301+ * @return void
302+ */
303+ private function removeSecureEnvironmentValue (string $ key ): void
304+ {
305+ unset($ this ->secureEnvStorage [$ key ]);
306+ putenv ($ key );
307+ }
104308}
0 commit comments