Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions .github/workflows/functional-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ jobs:
bin/magento mageforge:hyva:compatibility:check --show-all

echo "Third party only:"
bin/magento m:h:c:c --third-party-only
bin/magento mageforge:hyva:compatibility:check --third-party-only

echo "Detailed output:"
bin/magento m:h:c:c --show-all --detailed
bin/magento mageforge:hyva:compatibility:check --show-all --detailed

- name: Test Theme Cleaner
working-directory: magento2
Expand All @@ -139,7 +139,6 @@ jobs:
bin/magento mageforge:theme:clean --all --dry-run

echo "Test aliases:"
bin/magento m:t:c --help
bin/magento frontend:clean --help

- name: Test Theme Name Suggestions
Expand All @@ -155,11 +154,18 @@ jobs:
echo "CleanCommand with invalid name:"
bin/magento mageforge:theme:clean Magent/lum --dry-run || echo "Expected failure - suggestions shown"

- name: Test Inspector Status
- name: Test Copy From Vendor
working-directory: magento2
run: |
echo "=== Inspector Tests ==="
bin/magento mageforge:theme:inspector status
echo "=== Copy From Vendor Tests ==="

echo "Test help command:"
bin/magento mageforge:theme:copy-from-vendor --help

echo "Test alias:"
bin/magento theme:copy --help

echo "✓ Copy from vendor command and alias available"

- name: Test Inspector Functionality
working-directory: magento2
Expand Down Expand Up @@ -496,7 +502,6 @@ jobs:
bin/magento mageforge:theme:build Magento/blank --verbose || echo "Build attempted (may need additional setup)"

echo "Test build aliases:"
bin/magento m:t:b --help
bin/magento frontend:build --help

- name: Test Summary
Expand Down
28 changes: 9 additions & 19 deletions .github/workflows/magento-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

elasticsearch:
image: elasticsearch:7.17.0
ports:
- 9200:9200
image: elasticsearch:7.17.25
env:
discovery.type: single-node
ES_JAVA_OPTS: -Xms512m -Xmx512m
options: --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10
xpack.security.enabled: false
ports:
- 9200:9200
options: --health-cmd="curl -s http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10

steps:
- name: Checkout code
Expand Down Expand Up @@ -135,19 +135,14 @@ jobs:
bin/magento mageforge:theme:inspector --help
bin/magento mageforge:hyva:compatibility:check --help
bin/magento mageforge:hyva:tokens --help
bin/magento mageforge:theme:copy-from-vendor --help

echo "Verify command aliases work:"
bin/magento m:s:v --help
bin/magento m:s:c --help
bin/magento m:t:l --help
bin/magento m:t:b --help
bin/magento m:t:w --help
bin/magento m:t:c --help
bin/magento m:h:c:c --help
bin/magento frontend:list --help
bin/magento frontend:build --help
bin/magento frontend:watch --help
bin/magento frontend:clean --help
bin/magento theme:copy --help
bin/magento hyva:check --help
bin/magento hyva:tokens --help

Expand Down Expand Up @@ -273,19 +268,14 @@ jobs:
bin/magento mageforge:theme:inspector --help
bin/magento mageforge:hyva:compatibility:check --help
bin/magento mageforge:hyva:tokens --help
bin/magento mageforge:theme:copy-from-vendor --help

echo "Verify command aliases work:"
bin/magento m:s:v --help
bin/magento m:s:c --help
bin/magento m:t:l --help
bin/magento m:t:b --help
bin/magento m:t:w --help
bin/magento m:t:c --help
bin/magento m:h:c:c --help
bin/magento frontend:list --help
bin/magento frontend:build --help
bin/magento frontend:watch --help
bin/magento frontend:clean --help
bin/magento theme:copy --help
bin/magento hyva:check --help
bin/magento hyva:tokens --help

Expand Down
217 changes: 217 additions & 0 deletions src/Console/Command/Theme/CopyFromVendorCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

declare(strict_types=1);

namespace OpenForgeProject\MageForge\Console\Command\Theme;

use InvalidArgumentException;
use Laravel\Prompts\SearchPrompt;
use Magento\Framework\Console\Cli;
use Magento\Framework\Filesystem\DirectoryList;
use Magento\Framework\Component\ComponentRegistrar;
use Magento\Framework\Component\ComponentRegistrarInterface;
use OpenForgeProject\MageForge\Console\Command\AbstractCommand;
use OpenForgeProject\MageForge\Model\ThemeList;
use OpenForgeProject\MageForge\Service\VendorFileMapper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use function Laravel\Prompts\search;

class CopyFromVendorCommand extends AbstractCommand
{
public function __construct(
private readonly ThemeList $themeList,
private readonly VendorFileMapper $vendorFileMapper,
private readonly DirectoryList $directoryList,
private readonly ComponentRegistrarInterface $componentRegistrar
) {
parent::__construct();
}

protected function configure(): void
{
$this->setName('mageforge:theme:copy-from-vendor')
->setDescription('Copy a file from vendor/ to a specific theme with correct path resolution')
->setAliases(['theme:copy'])
->addArgument('file', InputArgument::REQUIRED, 'Path to the source file (vendor/...)')
->addArgument('theme', InputArgument::OPTIONAL, 'Target theme code (e.g. Magento/luma)')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Preview the copy operation without performing it');
}

protected function executeCommand(InputInterface $input, OutputInterface $output): int
{
try {
$sourceFileArg = $input->getArgument('file');
$isDryRun = $input->getOption('dry-run');
$absoluteSourcePath = $this->getAbsoluteSourcePath($sourceFileArg);

// Update sourceFileArg if it was normalized to relative path
$rootPath = $this->directoryList->getRoot();
$sourceFile = str_starts_with($absoluteSourcePath, $rootPath . '/')
? substr($absoluteSourcePath, strlen($rootPath) + 1)
: $sourceFileArg;

$themeCode = $this->getThemeCode($input);
$themePath = $this->getThemePath($themeCode);

$destinationPath = $this->vendorFileMapper->mapToThemePath($sourceFile, $themePath);
$absoluteDestPath = $this->getAbsoluteDestPath($destinationPath, $rootPath);

if ($isDryRun) {
$this->showDryRunPreview($sourceFile, $absoluteDestPath, $rootPath);
return Cli::RETURN_SUCCESS;
}

if (!$this->confirmCopy($sourceFile, $absoluteDestPath, $rootPath)) {
return Cli::RETURN_SUCCESS;
}

$this->performCopy($absoluteSourcePath, $absoluteDestPath);
$this->io->success("File copied successfully.");

return Cli::RETURN_SUCCESS;
} catch (\Exception $e) {
$this->io->error($e->getMessage());
return Cli::RETURN_FAILURE;
}
}

private function getAbsoluteSourcePath(string $sourceFile): string
{
$rootPath = $this->directoryList->getRoot();
if (str_starts_with($sourceFile, '/')) {
$absoluteSourcePath = $sourceFile;
} else {
$absoluteSourcePath = $rootPath . '/' . $sourceFile;
}

if (!file_exists($absoluteSourcePath)) {
throw new \RuntimeException("Source file not found: $absoluteSourcePath");
}

return $absoluteSourcePath;
}

private function getThemeCode(InputInterface $input): string
{
$themeCode = $input->getArgument('theme');
if ($themeCode) {
return $themeCode;
}

$themes = $this->themeList->getAllThemes();
$options = [];
foreach ($themes as $theme) {
$options[$theme->getCode()] = $theme->getCode();
}

if (empty($options)) {
throw new \RuntimeException('No themes found to copy to.');
}

$this->fixPromptEnvironment();

return (string) search(
label: 'Select target theme',
options: fn (string $value) => array_filter(
$options,
fn ($option) => str_contains(strtolower($option), strtolower($value))
),
placeholder: 'Search for a theme...'
);
}

private function getThemePath(string $themeCode): string
{
$theme = $this->themeList->getThemeByCode($themeCode);
if (!$theme) {
throw new \RuntimeException("Theme not found: $themeCode");
}

$regName = $theme->getArea() . '/' . $theme->getCode();
$themePath = $this->componentRegistrar->getPath(ComponentRegistrar::THEME, $regName);

if (!$themePath) {
$this->io->warning("Theme path not found via ComponentRegistrar for $regName, falling back to getFullPath()");
$themePath = $theme->getFullPath();
}

return $themePath;
}

private function getAbsoluteDestPath(string $destinationPath, string $rootPath): string
{
if (str_starts_with($destinationPath, '/')) {
return $destinationPath;
}
return $rootPath . '/' . $destinationPath;
}

private function confirmCopy(string $sourceFile, string $absoluteDestPath, string $rootPath): bool
{
$destinationDisplay = str_starts_with($absoluteDestPath, $rootPath . '/')
? substr($absoluteDestPath, strlen($rootPath) + 1)
: $absoluteDestPath;

$this->io->section('Copy Preview');
$this->io->text([
"Source: <info>$sourceFile</info>",
"Target: <info>$destinationDisplay</info>",
"Absolute Target: <comment>$absoluteDestPath</comment>"
]);
$this->io->newLine();

if (file_exists($absoluteDestPath)) {
$this->io->warning("File already exists at destination!");
return $this->io->confirm('Overwrite existing file?', false);
}

return $this->io->confirm('Proceed with copy?', true);
}

private function performCopy(string $absoluteSourcePath, string $absoluteDestPath): void
{
$directory = dirname($absoluteDestPath);
if (!is_dir($directory)) {
if (!mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $directory));
}
}
copy($absoluteSourcePath, $absoluteDestPath);
}

private function showDryRunPreview(string $sourceFile, string $absoluteDestPath, string $rootPath): void
{
$destinationDisplay = str_starts_with($absoluteDestPath, $rootPath . '/')
? substr($absoluteDestPath, strlen($rootPath) + 1)
: $absoluteDestPath;

$this->io->section('Dry Run - Copy Preview');
$this->io->text([
"Source: <info>$sourceFile</info>",
"Target: <info>$destinationDisplay</info>",
"Absolute Target: <comment>$absoluteDestPath</comment>"
]);
$this->io->newLine();

if (file_exists($absoluteDestPath)) {
$this->io->warning("File already exists at destination and would be overwritten!");
} else {
$this->io->info("File would be created at destination.");
}

$this->io->note("No files were modified (dry-run mode).");
}

private function fixPromptEnvironment(): void
{
if (getenv('DDEV_PROJECT')) {
putenv('COLUMNS=100');
putenv('LINES=40');
putenv('TERM=xterm-256color');
}
}
}
11 changes: 11 additions & 0 deletions src/Model/ThemeList.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,15 @@ public function getAllThemes(): array
{
return $this->magentoThemeList->getItems();
}

public function getThemeByCode(string $code): ?\Magento\Framework\View\Design\ThemeInterface
{
$themes = $this->getAllThemes();
foreach ($themes as $theme) {
if ($theme->getCode() === $code) {
return $theme;
}
}
return null;
}
}
Loading
Loading