From 35055bb0f8ee11a36efbbfcccca962542c0d908e Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 16 Mar 2026 11:42:11 +0100 Subject: [PATCH 1/5] feat: add wildcard theme resolution to build and clean commands --- src/Console/Command/AbstractCommand.php | 50 ++++++++++++++++++++++ src/Console/Command/Theme/BuildCommand.php | 30 ++++++++++--- src/Console/Command/Theme/CleanCommand.php | 9 ++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index 152ee54..288595f 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -478,4 +478,54 @@ private function removeSecureEnvironmentValue(string $name): void unset($this->secureEnvStorage[$name]); $this->clearEnvironmentCache(); } + + /** + * Resolve wildcard theme codes (e.g., Vendor/* to all underlying vendor themes) + * + * @param string[] $themeCodes + * @param \OpenForgeProject\MageForge\Model\ThemeList $themeList + * @return string[] + */ + protected function resolveWildcardThemes(array $themeCodes, \OpenForgeProject\MageForge\Model\ThemeList $themeList): array + { + $resolved = []; + $availableThemes = null; + + foreach ($themeCodes as $code) { + if (\str_ends_with($code, '/*')) { + // Lazy-load themes only when needed + if ($availableThemes === null) { + $availableThemes = array_map( + fn($theme) => $theme->getCode(), + $themeList->getAllThemes() + ); + } + + $prefix = substr($code, 0, -1); // Keeps the trailing slash, e.g. "Vendor/" + + $matched = array_filter( + $availableThemes, + fn(string $availableCode) => \str_starts_with($availableCode, $prefix) + ); + + if (empty($matched)) { + $this->io->warning(sprintf("No themes found for prefix '%s'", $prefix)); + } else { + $this->io->note(sprintf( + "Resolved '%s' to %d theme(s): %s", + $code, + count($matched), + implode(', ', $matched) + )); + } + + $resolved = array_merge($resolved, $matched); + } else { + $resolved[] = $code; + } + } + + // Return a fresh list without duplicates + return array_values(array_unique($resolved)); + } } diff --git a/src/Console/Command/Theme/BuildCommand.php b/src/Console/Command/Theme/BuildCommand.php index f70e506..3e3a6ce 100644 --- a/src/Console/Command/Theme/BuildCommand.php +++ b/src/Console/Command/Theme/BuildCommand.php @@ -66,6 +66,17 @@ protected function configure(): void protected function executeCommand(InputInterface $input, OutputInterface $output): int { $themeCodes = $input->getArgument('themeCodes'); + + // Allow wildcards using the AbstractCommand helper + if (!empty($themeCodes)) { + $themeCodes = $this->resolveWildcardThemes($themeCodes, $this->themeList); + + // If wildcards matched nothing and no other explicit themes remain + if (empty($themeCodes)) { + return Command::SUCCESS; + } + } + $isVerbose = $this->isVerbose($output); if (empty($themeCodes)) { @@ -337,12 +348,19 @@ private function processTheme( private function displayBuildSummary(SymfonyStyle $io, array $successList, float $duration): void { $io->newLine(); - $io->success(sprintf('🚀 Build process completed in %.2f seconds with the following results:', $duration)); - $io->writeln('Summary:'); - $io->newLine(); - - if (empty($successList)) { - $io->warning('No themes were built successfully.'); + + $successCount = count($successList); + + if ($successCount > 0) { + $io->success(sprintf( + '🚀 Successfully built %d theme(s). Build process completed in %.2f seconds.', + $successCount, + $duration + )); + $io->writeln('Summary:'); + $io->newLine(); + } else { + $io->warning(sprintf('Build process completed in %.2f seconds, but no themes were built successfully.', $duration)); return; } diff --git a/src/Console/Command/Theme/CleanCommand.php b/src/Console/Command/Theme/CleanCommand.php index 3bc29d9..85b8061 100644 --- a/src/Console/Command/Theme/CleanCommand.php +++ b/src/Console/Command/Theme/CleanCommand.php @@ -105,6 +105,15 @@ private function resolveThemeCodes(InputInterface $input, OutputInterface $outpu return $this->getAllThemeCodes(); } + if (!empty($themeCodes)) { + $themeCodes = $this->resolveWildcardThemes($themeCodes, $this->themeList); + + // If wildcards matched nothing and no other explicit themes remain + if (empty($themeCodes)) { + return null; + } + } + if (empty($themeCodes)) { return $this->selectThemesInteractively($output); } From 0407740986a248c010c84975148128a7394e9d10 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 16 Mar 2026 14:11:46 +0100 Subject: [PATCH 2/5] fix: improve theme resolution and output formatting in build command --- src/Console/Command/AbstractCommand.php | 8 +++++--- src/Console/Command/Theme/BuildCommand.php | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index 288595f..0ac7803 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -486,8 +486,10 @@ private function removeSecureEnvironmentValue(string $name): void * @param \OpenForgeProject\MageForge\Model\ThemeList $themeList * @return string[] */ - protected function resolveWildcardThemes(array $themeCodes, \OpenForgeProject\MageForge\Model\ThemeList $themeList): array - { + protected function resolveWildcardThemes( + array $themeCodes, + \OpenForgeProject\MageForge\Model\ThemeList $themeList + ): array { $resolved = []; $availableThemes = null; @@ -519,7 +521,7 @@ protected function resolveWildcardThemes(array $themeCodes, \OpenForgeProject\Ma )); } - $resolved = array_merge($resolved, $matched); + array_push($resolved, ...$matched); } else { $resolved[] = $code; } diff --git a/src/Console/Command/Theme/BuildCommand.php b/src/Console/Command/Theme/BuildCommand.php index 3e3a6ce..de398c8 100644 --- a/src/Console/Command/Theme/BuildCommand.php +++ b/src/Console/Command/Theme/BuildCommand.php @@ -353,14 +353,17 @@ private function displayBuildSummary(SymfonyStyle $io, array $successList, float if ($successCount > 0) { $io->success(sprintf( - '🚀 Successfully built %d theme(s). Build process completed in %.2f seconds.', - $successCount, + '🚀 Successfully built %d theme(s). Build process completed in %.2f seconds.', + $successCount, $duration )); $io->writeln('Summary:'); $io->newLine(); } else { - $io->warning(sprintf('Build process completed in %.2f seconds, but no themes were built successfully.', $duration)); + $io->warning(sprintf( + 'Build process completed in %.2f seconds, but no themes were built successfully.', + $duration + )); return; } From cd33ab0a71298ca13628e600e25e5465d4ae8c4a Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 18 Mar 2026 15:52:09 +0100 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Console/Command/AbstractCommand.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index 0ac7803..51837c2 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -7,6 +7,7 @@ use Laravel\Prompts\SelectPrompt; use Magento\Framework\Console\Cli; use OpenForgeProject\MageForge\Service\ThemeSuggester; +use OpenForgeProject\MageForge\Model\ThemeList; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -482,13 +483,13 @@ private function removeSecureEnvironmentValue(string $name): void /** * Resolve wildcard theme codes (e.g., Vendor/* to all underlying vendor themes) * - * @param string[] $themeCodes - * @param \OpenForgeProject\MageForge\Model\ThemeList $themeList - * @return string[] + * @param array $themeCodes + * @param ThemeList $themeList + * @return array */ protected function resolveWildcardThemes( array $themeCodes, - \OpenForgeProject\MageForge\Model\ThemeList $themeList + ThemeList $themeList ): array { $resolved = []; $availableThemes = null; From 97bb7f9706883dffe253767c3336a1ddaa3fca48 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 18 Mar 2026 16:47:41 +0100 Subject: [PATCH 4/5] feat: support multiple theme codes for build and clean commands --- docs/commands.md | 5 +++- src/Console/Command/AbstractCommand.php | 34 ++++++++++++++++------ src/Console/Command/Theme/BuildCommand.php | 10 +++---- src/Console/Command/Theme/CleanCommand.php | 6 ++-- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 07c2606..00925e0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -56,6 +56,7 @@ bin/magento mageforge:theme:build [...] **Implementation Details**: +- `themeCodes` accepts single themes (`Vendor/theme`) or just the vendor name (`Vendor`) to target all themes of a specific vendor. - If no theme codes are provided, displays an interactive prompt to select themes - For each selected theme: 1. Resolves the theme path @@ -109,11 +110,13 @@ bin/magento mageforge:theme:watch [--theme=THEME] **Usage**: ```bash -bin/magento mageforge:theme:clean [] +bin/magento mageforge:theme:clean [...] ``` **Implementation Details**: +- Can accept multiple themes like `Vendor/theme1 Vendor/theme2`. +- Accepts simply the vendor name `Vendor` to clean all registered themes for a vendor. - If no theme name is provided: - In interactive terminals, displays an interactive prompt to select the theme to clean - In non-interactive environments, prints the list of available themes and exits, requiring an explicit theme name diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index 51837c2..561402d 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -481,13 +481,13 @@ private function removeSecureEnvironmentValue(string $name): void } /** - * Resolve wildcard theme codes (e.g., Vendor/* to all underlying vendor themes) + * Resolve vendor theme codes (e.g., Vendor to all underlying vendor themes) * * @param array $themeCodes * @param ThemeList $themeList * @return array */ - protected function resolveWildcardThemes( + protected function resolveVendorThemes( array $themeCodes, ThemeList $themeList ): array { @@ -495,7 +495,11 @@ protected function resolveWildcardThemes( $availableThemes = null; foreach ($themeCodes as $code) { - if (\str_ends_with($code, '/*')) { + // Check if it's explicitly a wildcard OR just a vendor name without a slash + $isExplicitWildcard = \str_ends_with($code, '/*'); + $isVendorOnly = !\str_contains($code, '/'); + + if ($isExplicitWildcard || $isVendorOnly) { // Lazy-load themes only when needed if ($availableThemes === null) { $availableThemes = array_map( @@ -504,25 +508,37 @@ protected function resolveWildcardThemes( ); } - $prefix = substr($code, 0, -1); // Keeps the trailing slash, e.g. "Vendor/" - + if ($isExplicitWildcard) { + $prefix = substr($code, 0, -1); // Keeps the trailing slash, e.g. "Vendor/" + } else { + $prefix = $code . '/'; // e.g. "Vendor" -> "Vendor/" + } + $matched = array_filter( $availableThemes, fn(string $availableCode) => \str_starts_with($availableCode, $prefix) ); if (empty($matched)) { - $this->io->warning(sprintf("No themes found for prefix '%s'", $prefix)); + $this->io->warning(sprintf("No themes found for vendor/prefix '%s'", $prefix)); + + // If they typed just a word and it wasn't a vendor, + // we still add it so standard Magento validation kicks in later. + if ($isVendorOnly) { + $resolved[] = $code; + } } else { $this->io->note(sprintf( - "Resolved '%s' to %d theme(s): %s", + "Resolved vendor '%s' to %d theme(s): %s", $code, count($matched), implode(', ', $matched) )); - } - array_push($resolved, ...$matched); + foreach ($matched as $match) { + $resolved[] = $match; + } + } } else { $resolved[] = $code; } diff --git a/src/Console/Command/Theme/BuildCommand.php b/src/Console/Command/Theme/BuildCommand.php index de398c8..8d89536 100644 --- a/src/Console/Command/Theme/BuildCommand.php +++ b/src/Console/Command/Theme/BuildCommand.php @@ -51,7 +51,7 @@ protected function configure(): void ->addArgument( 'themeCodes', InputArgument::IS_ARRAY, - 'Theme codes to build (format: Vendor/theme, Vendor/theme 2, ...)', + 'Theme codes to build (format: Vendor/theme, Vendor, ...)', ) ->setAliases(['frontend:build']); } @@ -69,8 +69,8 @@ protected function executeCommand(InputInterface $input, OutputInterface $output // Allow wildcards using the AbstractCommand helper if (!empty($themeCodes)) { - $themeCodes = $this->resolveWildcardThemes($themeCodes, $this->themeList); - + $themeCodes = $this->resolveVendorThemes($themeCodes, $this->themeList); + // If wildcards matched nothing and no other explicit themes remain if (empty($themeCodes)) { return Command::SUCCESS; @@ -348,9 +348,9 @@ private function processTheme( private function displayBuildSummary(SymfonyStyle $io, array $successList, float $duration): void { $io->newLine(); - + $successCount = count($successList); - + if ($successCount > 0) { $io->success(sprintf( '🚀 Successfully built %d theme(s). Build process completed in %.2f seconds.', diff --git a/src/Console/Command/Theme/CleanCommand.php b/src/Console/Command/Theme/CleanCommand.php index 85b8061..053ac26 100644 --- a/src/Console/Command/Theme/CleanCommand.php +++ b/src/Console/Command/Theme/CleanCommand.php @@ -49,7 +49,7 @@ protected function configure(): void ->addArgument( 'themeCodes', InputArgument::IS_ARRAY, - 'Theme codes to clean (format: Vendor/theme, Vendor/theme 2, ...)', + 'Theme codes to clean (format: Vendor/theme, Vendor, ...)', ) ->addOption('all', 'a', InputOption::VALUE_NONE, 'Clean all themes') ->addOption( @@ -106,8 +106,8 @@ private function resolveThemeCodes(InputInterface $input, OutputInterface $outpu } if (!empty($themeCodes)) { - $themeCodes = $this->resolveWildcardThemes($themeCodes, $this->themeList); - + $themeCodes = $this->resolveVendorThemes($themeCodes, $this->themeList); + // If wildcards matched nothing and no other explicit themes remain if (empty($themeCodes)) { return null; From c97a06023a3721631700d65659527371c05ccb68 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 20 Mar 2026 08:07:15 +0100 Subject: [PATCH 5/5] docs: update pull request header to reflect upcoming release --- release-please-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-please-config.json b/release-please-config.json index 37aa103..f84408e 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -12,7 +12,7 @@ "include-v-in-tag": false, "include-component-in-tag": false, "pull-request-title-pattern": "chore: Next Release ${version}", - "pull-request-header": "## Release ${version}", + "pull-request-header": "## Upcoming Release", "changelog-sections": [ { "type": "feat",