From bada76f69c9850afb9473fc6af7ff5460d4a21fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sat, 25 Apr 2026 01:31:20 -0300 Subject: [PATCH 01/31] feat(composer): add command runtime hybrid foundation --- CHANGELOG.md | 4 + bin/dev-tools.php | 10 ++- .../Capability/DevToolsCommandProvider.php | 72 ++++++++++++++- src/Console/Command/AgentsCommand.php | 8 +- src/Console/Command/ProxyCommand.php | 58 ++++++++++++ .../CommandLoader/DevToolsCommandLoader.php | 12 ++- .../SymfonyDevToolsCommandLoader.php | 38 ++++++++ src/Console/DevTools.php | 3 +- src/Console/DevToolsComposer.php | 86 ++++++++++++++++++ .../DevToolsServiceProvider.php | 5 ++ .../DevToolsCommandProviderTest.php | 89 ++++++++++++++++++- tests/Console/Command/AgentsCommandTest.php | 22 +---- .../DevToolsCommandLoaderTest.php | 45 ++++++++-- 13 files changed, 412 insertions(+), 40 deletions(-) create mode 100644 src/Console/Command/ProxyCommand.php create mode 100644 src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php create mode 100644 src/Console/DevToolsComposer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd08bcdd..daf4c466e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add a hybrid command runtime bootstrap and capability bridge that keeps command discovery split between migrated Symfony commands (`DevTools`) and legacy Composer `BaseCommand` commands (`DevToolsComposer`) while exposing proxy commands during Composer execution for the first migration step (#199) + ## [1.22.3] - 2026-04-25 ### Fixed diff --git a/bin/dev-tools.php b/bin/dev-tools.php index c3fc7f46f..5ab7eaee5 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools; use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Console\DevToolsComposer; use Symfony\Component\Console\Input\ArgvInput; $projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; @@ -27,4 +28,11 @@ require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -DevTools::create()->run(new ArgvInput([...$argv, '--no-plugins'])); +$input = new ArgvInput([...$argv, '--no-plugins']); + +$command = $input->getFirstArgument(); +$application = (null !== $command && DevTools::create()->has($command)) + ? DevTools::create() + : DevToolsComposer::create(); + +$application->run($input); diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index 3322a87e8..c0b7c31e3 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -21,7 +21,10 @@ use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; +use FastForward\DevTools\Console\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Console\DevToolsComposer; +use Symfony\Component\Console\Command\Command; /** * Provides a registry of custom dev-tools commands mapped for Composer integration. @@ -34,9 +37,76 @@ final class DevToolsCommandProvider implements CommandProvider */ public function getCommands() { + $legacyCommands = DevToolsComposer::create()->all(); + $reservedCommandNames = $this->collectCommandNames($legacyCommands); + $migratedCommands = DevTools::create()->all(); + + $commands = $legacyCommands; + + foreach ($migratedCommands as $command) { + if (! $command instanceof Command || $command instanceof BaseCommand) { + continue; + } + + if ($this->hasReservedName($command, $reservedCommandNames)) { + continue; + } + + $commands[] = new ProxyCommand($command); + } + return array_values(array_filter( - DevTools::create()->all(), + $commands, static fn(object $command): bool => $command instanceof BaseCommand, )); } + + /** + * Collects command names and aliases that must remain mapped to legacy commands. + * + * @param array $commands + * + * @return array + */ + private function collectCommandNames(array $commands): array + { + $commandNames = []; + + foreach ($commands as $command) { + if (! $command instanceof BaseCommand) { + continue; + } + + if (null !== $command->getName()) { + $commandNames[$command->getName()] = true; + } + + foreach ($command->getAliases() as $alias) { + $commandNames[$alias] = true; + } + } + + return $commandNames; + } + + /** + * Verifies whether the command name or any aliases collide with legacy command names. + * + * @param Command $command + * @param array $reservedCommandNames + */ + private function hasReservedName(Command $command, array $reservedCommandNames): bool + { + if (null !== $command->getName() && isset($reservedCommandNames[$command->getName()])) { + return true; + } + + foreach ($command->getAliases() as $alias) { + if (isset($reservedCommandNames[$alias])) { + return true; + } + } + + return false; + } } diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index dc0cc2028..6e4e10c57 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -34,7 +34,7 @@ * Synchronizes packaged Fast Forward project agents into the consumer repository. */ #[AsCommand(name: 'agents', description: 'Synchronizes Fast Forward project agents into .agents/agents directory.')] -final class AgentsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class AgentsCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -51,7 +51,7 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly LoggerInterface $logger, ) { - parent::__construct(); + parent::__construct('agents'); } /** @@ -98,7 +98,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->logger->info('Created .agents/agents directory.'); } - $this->synchronizer->setLogger($this->getIO()); + $this->synchronizer->setLogger($this->logger); $result = $this->synchronizer->synchronize($agentsDir, $packageAgentsPath, self::AGENTS_DIRECTORY); diff --git a/src/Console/Command/ProxyCommand.php b/src/Console/Command/ProxyCommand.php new file mode 100644 index 000000000..9e7999443 --- /dev/null +++ b/src/Console/Command/ProxyCommand.php @@ -0,0 +1,58 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Console\Command; + +use Composer\Command\BaseCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Adapts migrated Symfony commands to Composer's BaseCommand contract. + */ +final class ProxyCommand extends BaseCommand +{ + public function __construct( + private readonly Command $command, + ) { + parent::__construct($command->getName()); + + $this->setAliases($command->getAliases()); + $this->setDescription($command->getDescription()); + $this->setHelp($command->getHelp()); + $this->setDefinition(clone $command->getDefinition()); + $this->setHidden($command->isHidden()); + $this->setIgnoreValidationErrors($command->ignoreValidationErrors()); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->command->setApplication($this->getApplication()); + $this->command->setHelperSet($this->getHelperSet()); + + return $this->command->run($input, $output); + } +} diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index c048b2344..93c4a834f 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -19,6 +19,7 @@ namespace FastForward\DevTools\Console\CommandLoader; +use Composer\Command\BaseCommand; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use Psr\Container\ContainerInterface; use ReflectionClass; @@ -35,6 +36,7 @@ * console commands and SHALL only register classes that: * - Are instantiable * - Extend the Symfony\Component\Console\Command\Command base class + * - Are not Composer\Command\BaseCommand-based command implementations * - Declare the Symfony\Component\Console\Attribute\AsCommand attribute * * The command name MUST be extracted from the AsCommand attribute metadata and @@ -53,9 +55,9 @@ final class DevToolsCommandLoader extends ContainerCommandLoader * @param FinderFactoryInterface $finderFactory * @param ContainerInterface $container */ - public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container) + public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container, bool $skipLegacyBaseCommands = false) { - parent::__construct($container, $this->getCommandMap($finderFactory)); + parent::__construct($container, $this->getCommandMap($finderFactory, $skipLegacyBaseCommands)); } /** @@ -65,7 +67,7 @@ public function __construct(FinderFactoryInterface $finderFactory, ContainerInte * * @return array */ - private function getCommandMap(FinderFactoryInterface $finderFactory): array + private function getCommandMap(FinderFactoryInterface $finderFactory, bool $skipLegacyBaseCommands): array { $commandMap = []; @@ -89,6 +91,10 @@ private function getCommandMap(FinderFactoryInterface $finderFactory): array continue; } + if ($skipLegacyBaseCommands && $reflection->isSubclassOf(BaseCommand::class)) { + continue; + } + $attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null; if (null === $attribute) { diff --git a/src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php b/src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php new file mode 100644 index 000000000..8c9f30830 --- /dev/null +++ b/src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php @@ -0,0 +1,38 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Console\CommandLoader; + +use FastForward\DevTools\Filesystem\FinderFactoryInterface; +use Psr\Container\ContainerInterface; + +/** + * Loads only migrated Symfony commands for the standalone DevTools application runtime. + */ +final class SymfonyDevToolsCommandLoader extends DevToolsCommandLoader +{ + /** + * @param FinderFactoryInterface $finderFactory + * @param ContainerInterface $container + */ + public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container) + { + parent::__construct($finderFactory, $container, true); + } +} diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 986836ddd..048dba7df 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -21,7 +21,6 @@ use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use Override; -use Composer\Console\Application as ComposerApplication; use DI\Container; use Psr\Container\ContainerInterface; use ReflectionMethod; @@ -32,7 +31,7 @@ * Wraps the fast-forward console tooling suite conceptually as an isolated application instance. * Extending the base application, it MUST provide default command injections safely. */ -final class DevTools extends ComposerApplication +final class DevTools extends Application { /** * @var ContainerInterface holds the static container instance for global access within the DevTools context diff --git a/src/Console/DevToolsComposer.php b/src/Console/DevToolsComposer.php new file mode 100644 index 000000000..00d1eabc8 --- /dev/null +++ b/src/Console/DevToolsComposer.php @@ -0,0 +1,86 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Console; + +use Composer\Console\Application; +use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; +use Override; +use DI\Container; +use Psr\Container\ContainerInterface; +use ReflectionMethod; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; + +/** + * Legacy composer-backed application that exposes non-migrated BaseCommand commands. + */ +final class DevToolsComposer extends Application +{ + /** + * @var ContainerInterface holds the shared container instance for global access within the DevTools composer context + */ + private static ?ContainerInterface $container = null; + + /** + * @param CommandLoaderInterface $commandLoader + */ + public function __construct(CommandLoaderInterface $commandLoader) + { + parent::__construct('Fast Forward Dev Tools'); + + $this->setDefaultCommand('standards'); + $this->setCommandLoader($commandLoader); + } + + /** + * Create DevToolsComposer instance from container. + * + * @return DevToolsComposer + */ + public static function create(): self + { + return self::getContainer()->get(self::class); + } + + /** + * Retrieves the shared DevTools service container. + */ + public static function getContainer(): ContainerInterface + { + if (! self::$container instanceof ContainerInterface) { + $serviceProvider = new DevToolsServiceProvider(); + self::$container = new Container($serviceProvider->getFactories()); + } + + return self::$container; + } + + /** + * Retrieves the default set of commands provided by the Composer Application. + * + * @return array + */ + #[Override] + protected function getDefaultCommands(): array + { + $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); + + return $reflectionMethod->invoke($this); + } +} diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index 4dbf8017c..821c03d85 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -34,7 +34,9 @@ use FastForward\DevTools\Changelog\Checker\UnreleasedEntryChecker; use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; +use FastForward\DevTools\Console\CommandLoader\SymfonyDevToolsCommandLoader; use FastForward\DevTools\Console\Formatter\LogLevelOutputFormatter; +use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Console\Logger\OutputFormatLogger; use FastForward\DevTools\Console\Logger\Processor\CommandInputProcessor; use FastForward\DevTools\Console\Logger\Processor\CommandOutputProcessor; @@ -84,6 +86,7 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Console\DevToolsComposer; use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Psr\Clock\SystemClock; use FastForward\DevTools\Resource\DifferInterface; @@ -158,6 +161,8 @@ public function getFactories(): array // Console CommandLoaderInterface::class => get(DevToolsCommandLoader::class), + DevTools::class => create(DevTools::class)->constructor(get(SymfonyDevToolsCommandLoader::class)), + DevToolsComposer::class => create(DevToolsComposer::class)->constructor(get(DevToolsCommandLoader::class)), CommandProvider::class => get(DevToolsCommandProvider::class), ConsoleOutputInterface::class => create(ConsoleOutput::class) ->method('setVerbosity', ConsoleOutputInterface::VERBOSITY_VERBOSE) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 5bddbda5a..ce4e894c9 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -21,7 +21,9 @@ use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; +use FastForward\DevTools\Console\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Console\DevToolsComposer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -42,6 +44,8 @@ final class DevToolsCommandProviderTest extends TestCase private ObjectProphecy $devTools; + private ObjectProphecy $devToolsComposer; + private DevToolsCommandProvider $commandProvider; /** @@ -51,18 +55,26 @@ protected function setUp(): void { $this->container = $this->prophesize(ContainerInterface::class); $this->devTools = $this->prophesize(DevTools::class); + $this->devToolsComposer = $this->prophesize(DevToolsComposer::class); $this->container->get(DevTools::class) ->willReturn($this->devTools->reveal()) ->shouldBeCalledOnce(); + $this->container->get(DevToolsComposer::class) + ->willReturn($this->devToolsComposer->reveal()) + ->shouldBeCalledOnce(); $this->devTools->all() ->willReturn([])->shouldBeCalledOnce(); + $this->devToolsComposer->all() + ->willReturn([])->shouldBeCalledOnce(); $this->commandProvider = new DevToolsCommandProvider(); $property = new ReflectionProperty(DevTools::class, 'container'); $property->setValue(null, $this->container->reveal()); + $propertyComposer = new ReflectionProperty(DevToolsComposer::class, 'container'); + $propertyComposer->setValue(null, $this->container->reveal()); } /** @@ -83,16 +95,85 @@ public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): vo #[Test] public function getCommandsWillReturnRegisteredBaseCommands(): void { - $composerCommand = $this->prophesize(BaseCommand::class)->reveal(); - $symfonyCommand = $this->prophesize(Command::class)->reveal(); + $composerCommand = new class extends BaseCommand { + public function __construct() + { + parent::__construct('legacy'); + } + + protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int + { + return self::SUCCESS; + } + }; + $symfonyCommand = new class extends Command { + public function __construct() + { + parent::__construct('agents'); + } + + protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int + { + return self::SUCCESS; + } + }; $this->devTools->all() - ->willReturn([$composerCommand, $symfonyCommand])->shouldBeCalledOnce(); + ->willReturn([$symfonyCommand])->shouldBeCalledOnce(); + $this->devToolsComposer->all() + ->willReturn([$composerCommand])->shouldBeCalledOnce(); $commands = $this->commandProvider->getCommands(); self::assertIsArray($commands); - self::assertCount(1, $commands); + self::assertCount(2, $commands); self::assertSame($composerCommand, $commands[0]); + self::assertInstanceOf(ProxyCommand::class, $commands[1]); + self::assertSame('agents', $commands[1]->getName()); + } + + /** + * @return void + */ + #[Test] + public function getCommandsWillSkipLegacyReservedSymfonyAliases(): void + { + $legacyCommand = new class extends BaseCommand { + public function __construct() + { + parent::__construct('migrated'); + } + + public function getAliases(): array + { + return ['agents-alias']; + } + + protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int + { + return self::SUCCESS; + } + }; + $symfonyCommand = new class extends Command { + public function __construct() + { + parent::__construct('migrated'); + } + + protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int + { + return self::SUCCESS; + } + }; + + $this->devTools->all() + ->willReturn([$symfonyCommand])->shouldBeCalledOnce(); + $this->devToolsComposer->all() + ->willReturn([$legacyCommand])->shouldBeCalledOnce(); + + $commands = $this->commandProvider->getCommands(); + + self::assertCount(1, $commands); + self::assertSame($legacyCommand, $commands[0]); } } diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php index edfc67db9..214a3741a 100644 --- a/tests/Console/Command/AgentsCommandTest.php +++ b/tests/Console/Command/AgentsCommandTest.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\Console\Application; -use Composer\IO\IOInterface; use FastForward\DevTools\Console\Command\AgentsCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -37,7 +35,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; use ReflectionMethod; -use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -62,10 +59,6 @@ final class AgentsCommandTest extends TestCase private ObjectProphecy $output; - private ObjectProphecy $application; - - private ObjectProphecy $io; - private AgentsCommand $command; /** @@ -78,13 +71,6 @@ protected function setUp(): void $this->logger = $this->prophesize(LoggerInterface::class); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); - $this->application = $this->prophesize(Application::class); - $this->io = $this->prophesize(IOInterface::class); - - $this->application->getHelperSet() - ->willReturn(new HelperSet()); - $this->application->getIO() - ->willReturn($this->io->reveal()); $this->filesystem->getAbsolutePath('.agents/agents') ->willReturn(getcwd() . '/.agents/agents'); @@ -93,7 +79,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->logger->reveal(), ); - $this->command->setApplication($this->application->reveal()); } /** @@ -106,7 +91,8 @@ public function executeWillFailWhenPackagedAgentsDirectoryDoesNotExist(): void $this->filesystem->exists($agentsPath) ->willReturn(false); - $this->synchronizer->setLogger(Argument::cetera())->shouldNotBeCalled(); + $this->synchronizer->setLogger($this->logger->reveal()) + ->shouldNotBeCalled(); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); $this->logger->info('Starting agents synchronization...') ->shouldBeCalledOnce(); @@ -138,7 +124,7 @@ public function executeWillCreateAgentsDirectoryWhenItDoesNotExist(): void ->willReturn(true, false); $this->filesystem->mkdir($agentsPath) ->shouldBeCalledOnce(); - $this->synchronizer->setLogger($this->io->reveal()) + $this->synchronizer->setLogger($this->logger->reveal()) ->shouldBeCalledOnce(); $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) @@ -173,7 +159,7 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->filesystem->exists($agentsPath) ->willReturn(true, true); - $this->synchronizer->setLogger($this->io->reveal()) + $this->synchronizer->setLogger($this->logger->reveal()) ->shouldBeCalledOnce(); $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index 7a5ac27c3..486d82f2f 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -20,7 +20,7 @@ namespace FastForward\DevTools\Tests\Console\CommandLoader; use ArrayIterator; -use FastForward\DevTools\Console\Command\CodeStyleCommand; +use FastForward\DevTools\Console\Command\AgentsCommand; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use PHPUnit\Framework\Attributes\CoversClass; @@ -36,7 +36,7 @@ use Symfony\Component\Finder\SplFileInfo; #[CoversClass(DevToolsCommandLoader::class)] -#[UsesClass(CodeStyleCommand::class)] +#[UsesClass(AgentsCommand::class)] final class DevToolsCommandLoaderTest extends TestCase { use ProphecyTrait; @@ -90,16 +90,47 @@ public function constructorWillRegisterOnlyInstantiableCommands(): void ->shouldBeCalled(); $this->finder->getIterator() ->willReturn(new ArrayIterator([ - new SplFileInfo($commandDirectory . '/CodeStyleCommand.php', '', 'CodeStyleCommand.php'), + new SplFileInfo($commandDirectory . '/AgentsCommand.php', '', 'AgentsCommand.php'), ]))->shouldBeCalled(); - $this->container->has(CodeStyleCommand::class)->willReturn(true)->shouldBeCalled(); - $this->container->get(CodeStyleCommand::class)->willReturn($command->reveal())->shouldBeCalled(); + $this->container->has(AgentsCommand::class)->willReturn(true)->shouldBeCalled(); + $this->container->get(AgentsCommand::class)->willReturn($command->reveal())->shouldBeCalled(); + + $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); + + self::assertTrue($loader->has('agents')); + self::assertSame($command->reveal(), $loader->get('agents')); + } + + /** + * @return void + */ + #[Test] + public function constructorWillSkipLegacyBaseCommands(): void + { + $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; + + $this->finderFactory->create() + ->willReturn($this->finder->reveal()) + ->shouldBeCalledOnce(); + $this->finder->files() + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled(); + $this->finder->notPath('Traits') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->name('*.php') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo($commandDirectory . '/CodeStyleCommand.php', '', 'CodeStyleCommand.php'), + ]))->shouldBeCalled(); $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); - self::assertTrue($loader->has('code-style')); - self::assertSame($command->reveal(), $loader->get('code-style')); + self::assertFalse($loader->has('code-style')); } /** From 5d4ee02c8a6de92aed6622617bc2e3653b14c84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sat, 25 Apr 2026 02:55:31 -0300 Subject: [PATCH 02/31] refactor composer json handling away from composer json utility classes --- src/Composer/Json/ComposerJson.php | 73 ++++++++++++-- .../Command/UpdateComposerJsonCommand.php | 95 ++++++++++++++----- src/Funding/ComposerFundingCodec.php | 23 ++++- 3 files changed, 153 insertions(+), 38 deletions(-) diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php index bdb6a5fd5..8bdd35035 100644 --- a/src/Composer/Json/ComposerJson.php +++ b/src/Composer/Json/ComposerJson.php @@ -20,19 +20,16 @@ namespace FastForward\DevTools\Composer\Json; use RuntimeException; -use UnexpectedValueException; -use Composer\Factory; use Composer\InstalledVersions; -use Composer\Json\JsonFile; use DateTimeImmutable; use FastForward\DevTools\Composer\Json\Schema\Author; use FastForward\DevTools\Composer\Json\Schema\AuthorInterface; use FastForward\DevTools\Composer\Json\Schema\Funding; use FastForward\DevTools\Composer\Json\Schema\Support; use FastForward\DevTools\Composer\Json\Schema\SupportInterface; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use UnderflowException; - -use function Safe\realpath; +use function Safe\json_decode; /** * Represents a specialized reader for a Composer JSON file. @@ -83,17 +80,17 @@ final class ComposerJson implements ComposerJsonInterface * default Composer file path SHALL be used. * * @throws RuntimeException when $path is'nt provided and COMPOSER environment variable is set to a directory - * @throws UnexpectedValueException when composer.json can't be parsed + * @throws RuntimeException when composer manifest files cannot be read or parsed */ public function __construct(?string $path = null) { - $pathLocal = realpath(Factory::getComposerFile()); + $pathLocal = WorkingProjectPathResolver::getProjectPath('composer.json'); $path ??= $pathLocal; $installedJsonPath = \dirname($pathLocal) . '/vendor/composer/installed.json'; - $this->data = (new JsonFile($path))->read(); - $this->installed = (new JsonFile($installedJsonPath))->read(); + $this->data = $this->readComposerJsonFile($path); + $this->installed = $this->readComposerInstalledManifest($installedJsonPath); } /** @@ -206,7 +203,7 @@ public function getReadme(): string */ public function getTime(): ?DateTimeImmutable { - $packages = $this->installed['packages']; + $packages = $this->installed['packages'] ?? []; if (isset($packages[$this->getName()])) { return new DateTimeImmutable($packages[$this->getName()]['time']); @@ -584,4 +581,60 @@ public function getComments(): array return \is_array($comments) ? $comments : []; } + + /** + * Reads and decodes a composer manifest file. + * + * @param string $path the manifest path + * + * @return array the parsed payload + */ + private function readComposerJsonFile(string $path): array + { + if (! file_exists($path)) { + throw new RuntimeException( + \sprintf('Unable to read composer manifest file at path: %s', $path), + ); + } + + return $this->decodeJson($path); + } + + /** + * Reads and decodes the composer installed manifest. + * + * @param string $path installed manifest path + * + * @return array the parsed payload + */ + private function readComposerInstalledManifest(string $path): array + { + if (! file_exists($path)) { + return []; + } + + return $this->decodeJson($path); + } + + /** + * Decodes a JSON file. + * + * @param string $path the file path + * + * @return array the decoded payload + */ + private function decodeJson(string $path): array + { + $contents = file_get_contents($path); + + if (false === $contents) { + throw new RuntimeException( + \sprintf('Unable to read composer manifest file at path: %s', $path), + ); + } + + $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR); + + return \is_array($data) ? $data : []; + } } diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index e4b4aa8fd..79d5e59a6 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -20,9 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; -use Composer\Factory; -use Composer\Json\JsonManipulator; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -32,11 +29,16 @@ use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Path; +use function Safe\json_decode; +use function Safe\json_encode; use function Safe\getcwd; /** @@ -46,7 +48,7 @@ name: 'update-composer-json', description: 'Updates composer.json with Fast Forward dev-tools scripts and metadata.' )] -final class UpdateComposerJsonCommand extends BaseCommand implements LoggerAwareCommandInterface +final class UpdateComposerJsonCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -59,6 +61,7 @@ final class UpdateComposerJsonCommand extends BaseCommand implements LoggerAware * @param FileLocatorInterface $fileLocator the locator used to resolve packaged configuration files * @param FileDiffer $fileDiffer * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly ComposerJsonInterface $composer, @@ -66,6 +69,7 @@ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -86,7 +90,7 @@ protected function configure(): void shortcut: 'f', mode: InputOption::VALUE_OPTIONAL, description: 'Path to the composer.json file to update.', - default: Factory::getComposerFile(), + default: 'composer.json', ) ->addOption( name: 'dry-run', @@ -132,22 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $currentContents = $this->filesystem->readFile($file); - $manipulator = new JsonManipulator($currentContents); - $grumphpConfig = DevToolsPathResolver::getPackagePath('grumphp.yml'); - - foreach ($this->scripts() as $name => $command) { - $manipulator->addSubNode('scripts', $name, $command); - } - - if ('' === $this->composer->getReadme() && $this->filesystem->exists('README.md', \dirname($file))) { - $manipulator->addProperty('readme', 'README.md'); - } - - $manipulator->addSubNode('extra', 'grumphp', [ - 'config-default-path' => Path::makeRelative($grumphpConfig, getcwd()), - ], true); - - $updatedContents = $manipulator->getContents(); + $updatedContents = $this->updatedComposerJsonContents($currentContents, $file); $comparison = $this->fileDiffer->diffContents( 'generated dev-tools composer.json configuration', $file, @@ -205,8 +194,68 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldUpdateComposerJson(string $file): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $file), false); + $confirmationMessage = \sprintf( + 'composer.json file %s has changes. Do you want to update it with the new dev-tools configuration?', + $file, + ); + + $confirmation = new ConfirmationQuestion($confirmationMessage, false); + + return $this->io->askQuestion($confirmation); + } + + /** + * Builds the managed composer.json payload. + * + * @param string $currentContents the current composer.json file contents + * @param string $file the path being updated, used to resolve local README checks + * + * @return string the composer.json payload with managed sections applied + */ + private function updatedComposerJsonContents(string $currentContents, string $file): string + { + $composerJsonData = json_decode($currentContents, true, 512, \JSON_THROW_ON_ERROR); + + if (! \is_array($composerJsonData)) { + $composerJsonData = []; + } + + $scripts = $composerJsonData['scripts'] ?? []; + if (! \is_array($scripts)) { + $scripts = []; + } + + foreach ($this->scripts() as $name => $command) { + $scripts[$name] = $command; + } + + $composerJsonData['scripts'] = $scripts; + + if ('' === $this->composer->getReadme() && $this->filesystem->exists('README.md', \dirname($file))) { + if (! isset($composerJsonData['readme'])) { + $composerJsonData['readme'] = 'README.md'; + } + } + + $extra = $composerJsonData['extra'] ?? []; + if (! \is_array($extra)) { + $extra = []; + } + + $grumphpConfig = DevToolsPathResolver::getPackagePath('grumphp.yml'); + $grumphpExtra = $extra['grumphp'] ?? []; + if (! \is_array($grumphpExtra)) { + $grumphpExtra = []; + } + + $grumphpExtra['config-default-path'] = Path::makeRelative($grumphpConfig, getcwd()); + $extra['grumphp'] = $grumphpExtra; + $composerJsonData['extra'] = $extra; + + return json_encode( + $composerJsonData, + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE + ) . "\n"; } /** diff --git a/src/Funding/ComposerFundingCodec.php b/src/Funding/ComposerFundingCodec.php index 59eeb4790..c9e62e3cf 100644 --- a/src/Funding/ComposerFundingCodec.php +++ b/src/Funding/ComposerFundingCodec.php @@ -19,12 +19,11 @@ namespace FastForward\DevTools\Funding; -use Composer\Json\JsonFile; - -use function Safe\json_encode; use function Safe\parse_url; use function Safe\preg_match; use function array_values; +use function Safe\json_decode; +use function Safe\json_encode; use function trim; /** @@ -41,7 +40,7 @@ */ public function parse(string $contents): FundingProfile { - $data = JsonFile::parseJson($contents); + $data = $this->decodeJsonContents($contents); $funding = $data['funding'] ?? []; if (! \is_array($funding)) { @@ -120,7 +119,7 @@ public function dump(string $contents, FundingProfile $profile): string $entries[] = $unsupportedEntry; } - $data = JsonFile::parseJson($contents); + $data = $this->decodeJsonContents($contents); unset($data['funding']); if ([] === $entries) { @@ -188,4 +187,18 @@ private function insertFundingEntries(array $data, array $entries): array return $orderedData; } + + /** + * Decodes a Composer JSON payload to an array. + * + * @param string $contents the JSON source + * + * @return array the decoded payload + */ + private function decodeJsonContents(string $contents): array + { + $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR); + + return \is_array($data) ? $data : []; + } } From b147d85ae70819fb790704eb3836a598ec909910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sat, 25 Apr 2026 03:05:50 -0300 Subject: [PATCH 03/31] refactor: complete composer-command migration and cleanup for dev-tools --- bin/dev-tools.php | 11 +- docs/commands/funding.rst | 2 +- .../Capability/DevToolsCommandProvider.php | 78 +------------ .../Command/ProxyCommand.php | 24 ++-- src/Composer/Json/ComposerJson.php | 10 +- src/Console/Command/AgentsCommand.php | 2 - src/Console/Command/ChangelogCheckCommand.php | 4 +- src/Console/Command/ChangelogEntryCommand.php | 4 +- .../Command/ChangelogNextVersionCommand.php | 4 +- .../Command/ChangelogPromoteCommand.php | 4 +- src/Console/Command/ChangelogShowCommand.php | 4 +- src/Console/Command/CodeOwnersCommand.php | 29 +++-- src/Console/Command/CodeStyleCommand.php | 4 +- src/Console/Command/CopyResourceCommand.php | 16 ++- src/Console/Command/DependenciesCommand.php | 4 +- src/Console/Command/DocsCommand.php | 4 +- src/Console/Command/FundingCommand.php | 16 ++- src/Console/Command/GitAttributesCommand.php | 13 ++- src/Console/Command/GitHooksCommand.php | 16 ++- src/Console/Command/GitIgnoreCommand.php | 13 ++- src/Console/Command/LicenseCommand.php | 13 ++- src/Console/Command/MetricsCommand.php | 4 +- src/Console/Command/PhpDocCommand.php | 4 +- src/Console/Command/RefactorCommand.php | 4 +- src/Console/Command/ReportsCommand.php | 13 ++- src/Console/Command/SkillsCommand.php | 8 +- src/Console/Command/StandardsCommand.php | 7 +- src/Console/Command/SyncCommand.php | 9 +- src/Console/Command/TestsCommand.php | 4 +- .../Command/UpdateComposerJsonCommand.php | 13 ++- src/Console/Command/WikiCommand.php | 6 +- .../CommandLoader/DevToolsCommandLoader.php | 22 ++-- .../SymfonyDevToolsCommandLoader.php | 38 ------ src/Console/DevTools.php | 18 --- src/Console/DevToolsComposer.php | 86 -------------- src/Path/DevToolsPathResolver.php | 12 ++ .../DevToolsServiceProvider.php | 10 +- src/Sync/PackagedDirectorySynchronizer.php | 11 +- .../DevToolsCommandProviderTest.php | 109 ++++-------------- tests/Console/Command/AgentsCommandTest.php | 6 - .../Console/Command/CodeOwnersCommandTest.php | 10 +- .../Command/CopyResourceCommandTest.php | 9 +- tests/Console/Command/FundingCommandTest.php | 11 +- .../Command/GitAttributesCommandTest.php | 9 +- tests/Console/Command/GitHooksCommandTest.php | 9 +- .../Console/Command/GitIgnoreCommandTest.php | 9 +- tests/Console/Command/LicenseCommandTest.php | 9 +- tests/Console/Command/SkillsCommandTest.php | 20 ---- .../Command/UpdateComposerJsonCommandTest.php | 9 +- .../DevToolsCommandLoaderTest.php | 11 +- .../PackagedDirectorySynchronizerTest.php | 35 +----- 51 files changed, 259 insertions(+), 541 deletions(-) rename src/{Console => Composer}/Command/ProxyCommand.php (68%) delete mode 100644 src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php delete mode 100644 src/Console/DevToolsComposer.php diff --git a/bin/dev-tools.php b/bin/dev-tools.php index 5ab7eaee5..d3eba1dae 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -20,19 +20,10 @@ namespace FastForward\DevTools; use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Console\DevToolsComposer; -use Symfony\Component\Console\Input\ArgvInput; $projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; $pluginVendorAutoload = \dirname(__DIR__) . '/vendor/autoload.php'; require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -$input = new ArgvInput([...$argv, '--no-plugins']); - -$command = $input->getFirstArgument(); -$application = (null !== $command && DevTools::create()->has($command)) - ? DevTools::create() - : DevToolsComposer::create(); - -$application->run($input); +DevTools::create()->run(); diff --git a/docs/commands/funding.rst b/docs/commands/funding.rst index ad95c8739..3910d6e65 100644 --- a/docs/commands/funding.rst +++ b/docs/commands/funding.rst @@ -33,7 +33,7 @@ Options ``--composer-file`` (optional) Path to the Composer manifest to synchronize. Default: - ``Factory::getComposerFile()``. + ``composer.json``. ``--funding-file`` (optional) Path to the GitHub funding file to synchronize. Default: diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index c0b7c31e3..0793ecbfa 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -21,9 +21,8 @@ use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\Console\Command\ProxyCommand; +use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Console\DevToolsComposer; use Symfony\Component\Console\Command\Command; /** @@ -37,76 +36,9 @@ final class DevToolsCommandProvider implements CommandProvider */ public function getCommands() { - $legacyCommands = DevToolsComposer::create()->all(); - $reservedCommandNames = $this->collectCommandNames($legacyCommands); - $migratedCommands = DevTools::create()->all(); - - $commands = $legacyCommands; - - foreach ($migratedCommands as $command) { - if (! $command instanceof Command || $command instanceof BaseCommand) { - continue; - } - - if ($this->hasReservedName($command, $reservedCommandNames)) { - continue; - } - - $commands[] = new ProxyCommand($command); - } - - return array_values(array_filter( - $commands, - static fn(object $command): bool => $command instanceof BaseCommand, - )); - } - - /** - * Collects command names and aliases that must remain mapped to legacy commands. - * - * @param array $commands - * - * @return array - */ - private function collectCommandNames(array $commands): array - { - $commandNames = []; - - foreach ($commands as $command) { - if (! $command instanceof BaseCommand) { - continue; - } - - if (null !== $command->getName()) { - $commandNames[$command->getName()] = true; - } - - foreach ($command->getAliases() as $alias) { - $commandNames[$alias] = true; - } - } - - return $commandNames; - } - - /** - * Verifies whether the command name or any aliases collide with legacy command names. - * - * @param Command $command - * @param array $reservedCommandNames - */ - private function hasReservedName(Command $command, array $reservedCommandNames): bool - { - if (null !== $command->getName() && isset($reservedCommandNames[$command->getName()])) { - return true; - } - - foreach ($command->getAliases() as $alias) { - if (isset($reservedCommandNames[$alias])) { - return true; - } - } - - return false; + return array_map( + static fn(Command $command): BaseCommand => new ProxyCommand($command), + DevTools::create()->all(), + ); } } diff --git a/src/Console/Command/ProxyCommand.php b/src/Composer/Command/ProxyCommand.php similarity index 68% rename from src/Console/Command/ProxyCommand.php rename to src/Composer/Command/ProxyCommand.php index 9e7999443..e9ab2c4df 100644 --- a/src/Console/Command/ProxyCommand.php +++ b/src/Composer/Command/ProxyCommand.php @@ -17,7 +17,7 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Console\Command; +namespace FastForward\DevTools\Composer\Command; use Composer\Command\BaseCommand; use Symfony\Component\Console\Command\Command; @@ -29,17 +29,20 @@ */ final class ProxyCommand extends BaseCommand { + /** + * @param Command $command the Symfony command adapted for Composer plugin execution + */ public function __construct( private readonly Command $command, ) { - parent::__construct($command->getName()); - - $this->setAliases($command->getAliases()); - $this->setDescription($command->getDescription()); - $this->setHelp($command->getHelp()); - $this->setDefinition(clone $command->getDefinition()); - $this->setHidden($command->isHidden()); - $this->setIgnoreValidationErrors($command->ignoreValidationErrors()); + parent::__construct($this->command->getName()); + + $this + ->setAliases($this->command->getAliases()) + ->setDescription($this->command->getDescription()) + ->setHelp($this->command->getHelp()) + ->setDefinition(clone $this->command->getDefinition()) + ->setHidden($this->command->isHidden()); } /** @@ -50,9 +53,6 @@ public function __construct( */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->command->setApplication($this->getApplication()); - $this->command->setHelperSet($this->getHelperSet()); - return $this->command->run($input, $output); } } diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php index 8bdd35035..8c3acb5a9 100644 --- a/src/Composer/Json/ComposerJson.php +++ b/src/Composer/Json/ComposerJson.php @@ -29,6 +29,8 @@ use FastForward\DevTools\Composer\Json\Schema\SupportInterface; use FastForward\DevTools\Path\WorkingProjectPathResolver; use UnderflowException; + +use function Safe\file_get_contents; use function Safe\json_decode; /** @@ -592,9 +594,7 @@ public function getComments(): array private function readComposerJsonFile(string $path): array { if (! file_exists($path)) { - throw new RuntimeException( - \sprintf('Unable to read composer manifest file at path: %s', $path), - ); + throw new RuntimeException(\sprintf('Unable to read composer manifest file at path: %s', $path)); } return $this->decodeJson($path); @@ -628,9 +628,7 @@ private function decodeJson(string $path): array $contents = file_get_contents($path); if (false === $contents) { - throw new RuntimeException( - \sprintf('Unable to read composer manifest file at path: %s', $path), - ); + throw new RuntimeException(\sprintf('Unable to read composer manifest file at path: %s', $path)); } $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR); diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index 6e4e10c57..7ab7f9a9e 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -98,8 +98,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->logger->info('Created .agents/agents directory.'); } - $this->synchronizer->setLogger($this->logger); - $result = $this->synchronizer->synchronize($agentsDir, $packageAgentsPath, self::AGENTS_DIRECTORY); if ($result->failed()) { diff --git a/src/Console/Command/ChangelogCheckCommand.php b/src/Console/Command/ChangelogCheckCommand.php index 4b9aef41a..af9544699 100644 --- a/src/Console/Command/ChangelogCheckCommand.php +++ b/src/Console/Command/ChangelogCheckCommand.php @@ -20,12 +20,12 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -37,7 +37,7 @@ name: 'changelog:check', description: 'Checks whether a changelog file contains meaningful unreleased entries.' )] -final class ChangelogCheckCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogCheckCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogEntryCommand.php b/src/Console/Command/ChangelogEntryCommand.php index 827489b2c..8a9a98763 100644 --- a/src/Console/Command/ChangelogEntryCommand.php +++ b/src/Console/Command/ChangelogEntryCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Document\ChangelogDocument; use FastForward\DevTools\Changelog\Entry\ChangelogEntryType; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; @@ -28,6 +27,7 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -40,7 +40,7 @@ name: 'changelog:entry', description: 'Adds a changelog entry to Unreleased or a specific version section.' )] -final class ChangelogEntryCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogEntryCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogNextVersionCommand.php b/src/Console/Command/ChangelogNextVersionCommand.php index f3837a8be..a68879566 100644 --- a/src/Console/Command/ChangelogNextVersionCommand.php +++ b/src/Console/Command/ChangelogNextVersionCommand.php @@ -21,12 +21,12 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Throwable; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -38,7 +38,7 @@ name: 'changelog:next-version', description: 'Infers the next semantic version from the Unreleased changelog section.' )] -final class ChangelogNextVersionCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogNextVersionCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogPromoteCommand.php b/src/Console/Command/ChangelogPromoteCommand.php index 4f7266ec0..8012456f6 100644 --- a/src/Console/Command/ChangelogPromoteCommand.php +++ b/src/Console/Command/ChangelogPromoteCommand.php @@ -21,13 +21,13 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Throwable; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -40,7 +40,7 @@ name: 'changelog:promote', description: 'Promotes Unreleased entries into a published changelog version.' )] -final class ChangelogPromoteCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogPromoteCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogShowCommand.php b/src/Console/Command/ChangelogShowCommand.php index 9d04e9488..167a40f43 100644 --- a/src/Console/Command/ChangelogShowCommand.php +++ b/src/Console/Command/ChangelogShowCommand.php @@ -21,12 +21,12 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Throwable; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -36,7 +36,7 @@ * Prints the rendered notes body for a released changelog version. */ #[AsCommand(name: 'changelog:show', description: 'Prints the notes body for a released changelog version.')] -final class ChangelogShowCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogShowCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/CodeOwnersCommand.php b/src/Console/Command/CodeOwnersCommand.php index 5173ac63a..fe969624c 100644 --- a/src/Console/Command/CodeOwnersCommand.php +++ b/src/Console/Command/CodeOwnersCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\CodeOwners\CodeOwnersGenerator; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -28,15 +27,18 @@ use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Generates and synchronizes CODEOWNERS files from local project metadata. */ #[AsCommand(name: 'codeowners', description: 'Generates .github/CODEOWNERS from local project metadata.')] -final class CodeOwnersCommand extends BaseCommand implements LoggerAwareCommandInterface +final class CodeOwnersCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -48,12 +50,14 @@ final class CodeOwnersCommand extends BaseCommand implements LoggerAwareCommandI * @param FilesystemInterface $filesystem the filesystem used to read and write the target file * @param FileDiffer $fileDiffer the differ used to report managed-file drift * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly CodeOwnersGenerator $generator, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -207,13 +211,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function promptForOwners(): array { - $answer = (string) $this->getIO() - ->ask( - 'No CODEOWNERS entries could be inferred from composer.json. Enter space-separated owners for "*" or leave blank to use a commented placeholder: ', - '', - ); + $answer = (string) $this->io->ask( + 'No CODEOWNERS entries could be inferred from composer.json. Enter space-separated owners for "*" or leave blank to use a commented placeholder: ', + ); - return $this->generator->normalizeOwners($answer); + return $this->generator->normalizeOwners($answer ?? ''); } /** @@ -225,7 +227,14 @@ private function promptForOwners(): array */ private function shouldWriteCodeOwners(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion( + \sprintf( + 'The generated CODEOWNERS file differs from the existing file at %s. Overwrite? [y/N] ', + $targetPath + ), + false, + ); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 28114575e..1e9e3e7d6 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -40,7 +40,7 @@ name: 'code-style', description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.' )] -final class CodeStyleCommand extends BaseCommand implements LoggerAwareCommandInterface +final class CodeStyleCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 329f69e41..6862f1662 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -29,16 +28,19 @@ use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Path; /** * Copies packaged or local resources into the consumer repository. */ #[AsCommand(name: 'copy-resource', description: 'Copies a file or directory resource into the current project.')] -final class CopyResourceCommand extends BaseCommand implements LoggerAwareCommandInterface +final class CopyResourceCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -51,6 +53,7 @@ final class CopyResourceCommand extends BaseCommand implements LoggerAwareComman * @param FinderFactoryInterface $finderFactory the factory used to create finders for directory resources * @param FileDiffer $fileDiffer the service used to summarize overwrite changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly FilesystemInterface $filesystem, @@ -58,6 +61,7 @@ public function __construct( private readonly FinderFactoryInterface $finderFactory, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -309,7 +313,11 @@ private function copyFile( */ private function shouldReplaceResource(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Replace drifted resource %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion( + \sprintf('Replace drifted resource %s? [y/N] ', $targetPath), + false, + ); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index e80c4481b..2cbcb46e4 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig; use FastForward\DevTools\Process\ProcessBuilderInterface; @@ -29,6 +28,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -47,7 +47,7 @@ description: 'Analyzes missing, unused, misplaced, and outdated Composer dependencies.', aliases: ['deps'] )] -final class DependenciesCommand extends BaseCommand implements LoggerAwareCommandInterface +final class DependenciesCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index a5822dfe3..ddba461c7 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -24,13 +24,13 @@ use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; use Twig\Environment; -use Composer\Command\BaseCommand; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -47,7 +47,7 @@ * command surface. */ #[AsCommand(name: 'docs', description: 'Generates API documentation.')] -final class DocsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class DocsCommand extends Command implements LoggerAwareCommandInterface { use HasCacheOption; use HasJsonOption; diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index 58d1f528e..dbed08075 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; -use Composer\Factory; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -32,9 +30,12 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Synchronizes funding metadata between composer.json and .github/FUNDING.yml. @@ -43,7 +44,7 @@ name: 'funding', description: 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.' )] -final class FundingCommand extends BaseCommand implements LoggerAwareCommandInterface +final class FundingCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -59,6 +60,7 @@ final class FundingCommand extends BaseCommand implements LoggerAwareCommandInte * @param ProcessBuilderInterface $processBuilder the process builder used to normalize composer.json after updates * @param ProcessQueueInterface $processQueue the process queue used to execute composer normalize * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly FilesystemInterface $filesystem, @@ -69,6 +71,7 @@ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -88,7 +91,7 @@ protected function configure(): void name: 'composer-file', mode: InputOption::VALUE_OPTIONAL, description: 'Path to the composer.json file to synchronize.', - default: Factory::getComposerFile(), + default: 'composer.json', ) ->addOption( name: 'funding-file', @@ -470,8 +473,9 @@ private function handleFundingFile( */ private function shouldWriteManagedFile(string $targetFile): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $targetFile), false); + $confirmation = new ConfirmationQuestion(\sprintf('Update managed file %s? [y/N] ', $targetFile), false); + + return $this->io->askQuestion($confirmation); } /** diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index 37f9677f6..7120676c2 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; @@ -33,9 +32,12 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use function Safe\getcwd; @@ -49,7 +51,7 @@ name: 'gitattributes', description: 'Manages .gitattributes export-ignore rules for leaner package archives.' )] -final class GitAttributesCommand extends BaseCommand implements LoggerAwareCommandInterface +final class GitAttributesCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -75,6 +77,7 @@ final class GitAttributesCommand extends BaseCommand implements LoggerAwareComma * @param ComposerJsonInterface $composer the composer.json accessor * @param FileDiffer $fileDiffer * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly CandidateProviderInterface $candidateProvider, @@ -87,6 +90,7 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -259,8 +263,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldWriteGitAttributes(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + + return $this->io->askQuestion($confirmation); } /** diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index 82c4b8d1f..2abf9ce60 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FinderFactoryInterface; @@ -28,9 +27,12 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Path; @@ -38,7 +40,7 @@ * Installs packaged Git hooks for the consumer repository. */ #[AsCommand(name: 'git-hooks', description: 'Installs Fast Forward Git hooks.')] -final class GitHooksCommand extends BaseCommand implements LoggerAwareCommandInterface +final class GitHooksCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -51,6 +53,7 @@ final class GitHooksCommand extends BaseCommand implements LoggerAwareCommandInt * @param FinderFactoryInterface $finderFactory the factory used to create finders for hook files * @param FileDiffer $fileDiffer * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly FilesystemInterface $filesystem, @@ -58,6 +61,7 @@ public function __construct( private readonly FinderFactoryInterface $finderFactory, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -259,8 +263,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldReplaceHook(string $hookPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Replace drifted Git hook %s? [y/N] ', $hookPath), false); + $confirmation = new ConfirmationQuestion( + \sprintf('Replace drifted Git hook %s? [y/N] ', $hookPath), + false, + ); + + return $this->io->askQuestion($confirmation); } /** diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index 92be06ebb..0a61a3276 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\GitIgnore\MergerInterface; use FastForward\DevTools\GitIgnore\ReaderInterface; @@ -31,9 +30,12 @@ use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Provides functionality to merge and synchronize .gitignore files. @@ -45,7 +47,7 @@ * to the canonical and project .gitignore files respectively. */ #[AsCommand(name: 'gitignore', description: 'Merges and synchronizes .gitignore files.')] -final class GitIgnoreCommand extends BaseCommand implements LoggerAwareCommandInterface +final class GitIgnoreCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -64,6 +66,7 @@ final class GitIgnoreCommand extends BaseCommand implements LoggerAwareCommandIn * @param FileLocatorInterface $fileLocator the file locator * @param FileDiffer $fileDiffer * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly MergerInterface $merger, @@ -72,6 +75,7 @@ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -214,7 +218,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldWriteGitIgnore(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index 79aa829a0..1c88e02d8 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -27,9 +26,12 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Generates and copies LICENSE files to projects. @@ -38,7 +40,7 @@ * license is declared in composer.json. */ #[AsCommand(name: 'license', description: 'Generates a LICENSE file from composer.json license information.')] -final class LicenseCommand extends BaseCommand implements LoggerAwareCommandInterface +final class LicenseCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -50,12 +52,14 @@ final class LicenseCommand extends BaseCommand implements LoggerAwareCommandInte * @param FilesystemInterface $filesystem the filesystem component * @param FileDiffer $fileDiffer * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io */ public function __construct( private readonly GeneratorInterface $generator, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -230,7 +234,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldWriteLicense(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion(\sprintf('Write managed file %s? [y/N] ', $targetPath), false); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 59471a96b..bceef3aa9 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -35,7 +35,7 @@ use function rtrim; #[AsCommand(name: 'metrics', description: 'Analyzes code metrics with PhpMetrics.')] -final class MetricsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class MetricsCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 75cf08129..46a422bbc 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -30,11 +30,11 @@ use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; use Twig\Environment; -use Composer\Command\BaseCommand; use Throwable; use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -45,7 +45,7 @@ * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ #[AsCommand(name: 'phpdoc', description: 'Checks and fixes PHPDocs.')] -final class PhpDocCommand extends BaseCommand implements LoggerAwareCommandInterface +final class PhpDocCommand extends Command implements LoggerAwareCommandInterface { use HasCacheOption; use HasJsonOption; diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index 8a7fb769d..af5dbf8b8 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -37,7 +37,7 @@ * This class MUST NOT be extended and SHALL encapsulate the logic for Rector invocation. */ #[AsCommand(name: 'refactor', description: 'Runs Rector for code refactoring.', aliases: ['rector'])] -final class RefactorCommand extends BaseCommand implements LoggerAwareCommandInterface +final class RefactorCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index d896c6afa..32c8dcef4 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -20,16 +20,17 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; -use Composer\Console\Input\InputOption; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -38,7 +39,7 @@ * This class MUST NOT be overridden and SHALL securely combine docs and testing commands. */ #[AsCommand(name: 'reports', description: 'Generates the frontpage for Fast Forward documentation.')] -final class ReportsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ReportsCommand extends Command implements LoggerAwareCommandInterface { use HasCacheOption; use HasJsonOption; @@ -165,7 +166,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $docsBuilder = $docsBuilder->withArgument('--pretty-json'); } - $docs = $docsBuilder->build('composer dev-tools docs --'); + $docs = $docsBuilder->build(DevToolsPathResolver::getBinaryCommand('docs')); if (null !== $cacheArgument) { $coverageBuilder = $coverageBuilder->withArgument($cacheArgument); @@ -187,7 +188,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $coverageBuilder = $coverageBuilder->withArgument('--pretty-json'); } - $coverage = $coverageBuilder->build('composer dev-tools tests --'); + $coverage = $coverageBuilder->build(DevToolsPathResolver::getBinaryCommand('tests')); if ($progress) { $metricsBuilder = $metricsBuilder->withArgument('--progress'); @@ -201,7 +202,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $metricsBuilder = $metricsBuilder->withArgument('--pretty-json'); } - $metrics = $metricsBuilder->build('composer dev-tools metrics --'); + $metrics = $metricsBuilder->build(DevToolsPathResolver::getBinaryCommand('metrics')); $this->processQueue->add(process: $docs, detached: true, label: 'Generating API Docs Report'); $this->processQueue->add(process: $coverage, label: 'Generating Coverage Report'); diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php index 5a0cf0be2..5a10a249e 100644 --- a/src/Console/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -45,7 +45,7 @@ * into Symfony Console output and process exit codes. */ #[AsCommand(name: 'skills', description: 'Synchronizes Fast Forward skills into .agents/skills directory.')] -final class SkillsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class SkillsCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; @@ -68,7 +68,7 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly LoggerInterface $logger, ) { - parent::__construct(); + parent::__construct('skills'); } /** @@ -130,8 +130,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->logger->info('Created .agents/skills directory.'); } - $this->synchronizer->setLogger($this->getIO()); - $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY); if ($result->failed()) { diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index e176644fb..7093f9fb4 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -20,14 +20,15 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -38,7 +39,7 @@ * This class MUST NOT be modified through inheritance and SHALL streamline code validation workflows. */ #[AsCommand(name: 'standards', description: 'Runs Fast Forward code standards checks.')] -final class StandardsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class StandardsCommand extends Command implements LoggerAwareCommandInterface { use HasCacheOption; use HasJsonOption; @@ -145,7 +146,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->processQueue->add( - process: $processBuilder->build('composer dev-tools ' . $command . ' --'), + process: $processBuilder->build(DevToolsPathResolver::getBinaryCommand($command)), label: $this->getProcessLabel($command), ); } diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index c08fd2414..644bd444c 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -36,10 +36,11 @@ * Orchestrates dev-tools synchronization commands for the consumer repository. */ #[AsCommand( - name: 'dev-tools:sync', - description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, CODEOWNERS, .editorconfig, and .gitattributes in the root project.' + name: 'synchronize', + description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, CODEOWNERS, .editorconfig, and .gitattributes in the root project.', + aliases: ['dev-tools:sync', 'sync'], )] -final class SyncCommand extends BaseCommand implements LoggerAwareCommandInterface +final class SyncCommand extends Command implements LoggerAwareCommandInterface { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index d62ba27b7..375cf6e1e 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; @@ -34,6 +33,7 @@ use RuntimeException; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -47,7 +47,7 @@ * This class MUST NOT be overridden and SHALL configure testing parameters dynamically. */ #[AsCommand(name: 'tests', description: 'Runs PHPUnit tests.')] -final class TestsCommand extends BaseCommand implements LoggerAwareCommandInterface +final class TestsCommand extends Command implements LoggerAwareCommandInterface { use HasCacheOption; use HasJsonOption; diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index 79d5e59a6..6f30b4ff5 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -225,16 +225,17 @@ private function updatedComposerJsonContents(string $currentContents, string $fi $scripts = []; } - foreach ($this->scripts() as $name => $command) { + foreach ($this->getScripts() as $name => $command) { $scripts[$name] = $command; } $composerJsonData['scripts'] = $scripts; - if ('' === $this->composer->getReadme() && $this->filesystem->exists('README.md', \dirname($file))) { - if (! isset($composerJsonData['readme'])) { - $composerJsonData['readme'] = 'README.md'; - } + if ('' === $this->composer->getReadme() && $this->filesystem->exists( + 'README.md', + \dirname($file) + ) && ! isset($composerJsonData['readme'])) { + $composerJsonData['readme'] = 'README.md'; } $extra = $composerJsonData['extra'] ?? []; @@ -263,7 +264,7 @@ private function updatedComposerJsonContents(string $currentContents, string $fi * * @return array the script name to command map */ - private function scripts(): array + private function getScripts(): array { return [ 'dev-tools' => 'dev-tools', diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 85b9b9119..81c549a44 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; @@ -31,6 +30,7 @@ use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -44,7 +44,7 @@ * This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task. */ #[AsCommand(name: 'wiki', description: 'Generates API documentation in Markdown format.')] -final class WikiCommand extends BaseCommand implements LoggerAwareCommandInterface +final class WikiCommand extends Command implements LoggerAwareCommandInterface { use HasCacheOption; use HasJsonOption; @@ -68,7 +68,7 @@ public function __construct( private readonly GitClientInterface $gitClient, private readonly LoggerInterface $logger, ) { - return parent::__construct(); + parent::__construct(); } /** diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index 93c4a834f..11713711e 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\CommandLoader; -use Composer\Command\BaseCommand; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use Psr\Container\ContainerInterface; use ReflectionClass; @@ -36,7 +35,6 @@ * console commands and SHALL only register classes that: * - Are instantiable * - Extend the Symfony\Component\Console\Command\Command base class - * - Are not Composer\Command\BaseCommand-based command implementations * - Declare the Symfony\Component\Console\Attribute\AsCommand attribute * * The command name MUST be extracted from the AsCommand attribute metadata and @@ -55,9 +53,9 @@ final class DevToolsCommandLoader extends ContainerCommandLoader * @param FinderFactoryInterface $finderFactory * @param ContainerInterface $container */ - public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container, bool $skipLegacyBaseCommands = false) + public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container) { - parent::__construct($container, $this->getCommandMap($finderFactory, $skipLegacyBaseCommands)); + parent::__construct($container, $this->getCommandMap($finderFactory)); } /** @@ -67,7 +65,7 @@ public function __construct(FinderFactoryInterface $finderFactory, ContainerInte * * @return array */ - private function getCommandMap(FinderFactoryInterface $finderFactory, bool $skipLegacyBaseCommands): array + private function getCommandMap(FinderFactoryInterface $finderFactory): array { $commandMap = []; @@ -91,10 +89,6 @@ private function getCommandMap(FinderFactoryInterface $finderFactory, bool $skip continue; } - if ($skipLegacyBaseCommands && $reflection->isSubclassOf(BaseCommand::class)) { - continue; - } - $attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null; if (null === $attribute) { @@ -102,7 +96,15 @@ private function getCommandMap(FinderFactoryInterface $finderFactory, bool $skip } $arguments = $attribute->getArguments(); - $commandMap[$arguments['name']] = $class; + $commandNames = [$arguments['name'], ...($arguments['aliases'] ?? [])]; + + foreach ($commandNames as $commandName) { + if ('' === $commandName) { + continue; + } + + $commandMap[$commandName] = $class; + } } return $commandMap; diff --git a/src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php b/src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php deleted file mode 100644 index 8c9f30830..000000000 --- a/src/Console/CommandLoader/SymfonyDevToolsCommandLoader.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Console\CommandLoader; - -use FastForward\DevTools\Filesystem\FinderFactoryInterface; -use Psr\Container\ContainerInterface; - -/** - * Loads only migrated Symfony commands for the standalone DevTools application runtime. - */ -final class SymfonyDevToolsCommandLoader extends DevToolsCommandLoader -{ - /** - * @param FinderFactoryInterface $finderFactory - * @param ContainerInterface $container - */ - public function __construct(FinderFactoryInterface $finderFactory, ContainerInterface $container) - { - parent::__construct($finderFactory, $container, true); - } -} diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 048dba7df..50abd31f5 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -20,10 +20,8 @@ namespace FastForward\DevTools\Console; use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; -use Override; use DI\Container; use Psr\Container\ContainerInterface; -use ReflectionMethod; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; @@ -76,20 +74,4 @@ public static function getContainer(): ContainerInterface return self::$container; } - - /** - * Retrieves the default set of commands provided by the Symfony Application. - * - * The method SHOULD NOT add composer-specific commands to the list, - * as they are handled separately by composer when loaded as a plugin. - * - * @return array - */ - #[Override] - protected function getDefaultCommands(): array - { - $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); - - return $reflectionMethod->invoke($this); - } } diff --git a/src/Console/DevToolsComposer.php b/src/Console/DevToolsComposer.php deleted file mode 100644 index 00d1eabc8..000000000 --- a/src/Console/DevToolsComposer.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Console; - -use Composer\Console\Application; -use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; -use Override; -use DI\Container; -use Psr\Container\ContainerInterface; -use ReflectionMethod; -use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; - -/** - * Legacy composer-backed application that exposes non-migrated BaseCommand commands. - */ -final class DevToolsComposer extends Application -{ - /** - * @var ContainerInterface holds the shared container instance for global access within the DevTools composer context - */ - private static ?ContainerInterface $container = null; - - /** - * @param CommandLoaderInterface $commandLoader - */ - public function __construct(CommandLoaderInterface $commandLoader) - { - parent::__construct('Fast Forward Dev Tools'); - - $this->setDefaultCommand('standards'); - $this->setCommandLoader($commandLoader); - } - - /** - * Create DevToolsComposer instance from container. - * - * @return DevToolsComposer - */ - public static function create(): self - { - return self::getContainer()->get(self::class); - } - - /** - * Retrieves the shared DevTools service container. - */ - public static function getContainer(): ContainerInterface - { - if (! self::$container instanceof ContainerInterface) { - $serviceProvider = new DevToolsServiceProvider(); - self::$container = new Container($serviceProvider->getFactories()); - } - - return self::$container; - } - - /** - * Retrieves the default set of commands provided by the Composer Application. - * - * @return array - */ - #[Override] - protected function getDefaultCommands(): array - { - $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); - - return $reflectionMethod->invoke($this); - } -} diff --git a/src/Path/DevToolsPathResolver.php b/src/Path/DevToolsPathResolver.php index 23778ed47..14f2f89a3 100644 --- a/src/Path/DevToolsPathResolver.php +++ b/src/Path/DevToolsPathResolver.php @@ -67,6 +67,18 @@ public static function getBinaryPath(): string return self::getPackagePath(self::BINARY); } + /** + * Returns the packaged DevTools binary command with a subcommand. + * + * @param string $command the DevTools subcommand to append + */ + public static function getBinaryCommand(string $command): string + { + $binaryPath = self::getPackagePath(self::BINARY); + + return \sprintf('%s %s', $binaryPath, $command); + } + /** * Returns the packaged resources directory or a path under it. * diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index 821c03d85..ec349aeec 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -34,7 +34,6 @@ use FastForward\DevTools\Changelog\Checker\UnreleasedEntryChecker; use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; -use FastForward\DevTools\Console\CommandLoader\SymfonyDevToolsCommandLoader; use FastForward\DevTools\Console\Formatter\LogLevelOutputFormatter; use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Console\Logger\OutputFormatLogger; @@ -86,7 +85,6 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator; use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Console\DevToolsComposer; use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Psr\Clock\SystemClock; use FastForward\DevTools\Resource\DifferInterface; @@ -99,8 +97,11 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Twig\Loader\FilesystemLoader; use Twig\Loader\LoaderInterface; @@ -160,9 +161,10 @@ public function getFactories(): array ClockInterface::class => get(SystemClock::class), // Console + InputInterface::class => get(ArgvInput::class), + OutputInterface::class => get(ConsoleOutputInterface::class), CommandLoaderInterface::class => get(DevToolsCommandLoader::class), - DevTools::class => create(DevTools::class)->constructor(get(SymfonyDevToolsCommandLoader::class)), - DevToolsComposer::class => create(DevToolsComposer::class)->constructor(get(DevToolsCommandLoader::class)), + DevTools::class => create(DevTools::class)->constructor(get(DevToolsCommandLoader::class)), CommandProvider::class => get(DevToolsCommandProvider::class), ConsoleOutputInterface::class => create(ConsoleOutput::class) ->method('setVerbosity', ConsoleOutputInterface::VERBOSITY_VERBOSE) diff --git a/src/Sync/PackagedDirectorySynchronizer.php b/src/Sync/PackagedDirectorySynchronizer.php index 521231e2c..2728c6c61 100644 --- a/src/Sync/PackagedDirectorySynchronizer.php +++ b/src/Sync/PackagedDirectorySynchronizer.php @@ -21,14 +21,13 @@ use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Path; /** * Synchronizes one packaged directory of symlinked entries into a consumer repository. */ -final class PackagedDirectorySynchronizer implements LoggerAwareInterface +final class PackagedDirectorySynchronizer { /** * Initializes the synchronizer with a filesystem and finder factory. @@ -43,14 +42,6 @@ public function __construct( private LoggerInterface $logger, ) {} - /** - * {@inheritDoc} - */ - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - /** * Synchronizes packaged directory entries into the consumer repository. * diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index ce4e894c9..b0ba09cdf 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -19,11 +19,9 @@ namespace FastForward\DevTools\Tests\Composer\Capability; -use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\Console\Command\ProxyCommand; +use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Console\DevToolsComposer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -33,6 +31,7 @@ use Psr\Container\ContainerInterface; use ReflectionProperty; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputDefinition; #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(DevTools::class)] @@ -44,8 +43,6 @@ final class DevToolsCommandProviderTest extends TestCase private ObjectProphecy $devTools; - private ObjectProphecy $devToolsComposer; - private DevToolsCommandProvider $commandProvider; /** @@ -55,26 +52,18 @@ protected function setUp(): void { $this->container = $this->prophesize(ContainerInterface::class); $this->devTools = $this->prophesize(DevTools::class); - $this->devToolsComposer = $this->prophesize(DevToolsComposer::class); $this->container->get(DevTools::class) ->willReturn($this->devTools->reveal()) ->shouldBeCalledOnce(); - $this->container->get(DevToolsComposer::class) - ->willReturn($this->devToolsComposer->reveal()) - ->shouldBeCalledOnce(); $this->devTools->all() ->willReturn([])->shouldBeCalledOnce(); - $this->devToolsComposer->all() - ->willReturn([])->shouldBeCalledOnce(); $this->commandProvider = new DevToolsCommandProvider(); $property = new ReflectionProperty(DevTools::class, 'container'); $property->setValue(null, $this->container->reveal()); - $propertyComposer = new ReflectionProperty(DevToolsComposer::class, 'container'); - $propertyComposer->setValue(null, $this->container->reveal()); } /** @@ -93,87 +82,33 @@ public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): vo * @return void */ #[Test] - public function getCommandsWillReturnRegisteredBaseCommands(): void + public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCommands(): void { - $composerCommand = new class extends BaseCommand { - public function __construct() - { - parent::__construct('legacy'); - } - - protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int - { - return self::SUCCESS; - } - }; - $symfonyCommand = new class extends Command { - public function __construct() - { - parent::__construct('agents'); - } - - protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int - { - return self::SUCCESS; - } - }; + $symfonyCommand = $this->prophesize(Command::class); + $inputDefinition = $this->prophesize(InputDefinition::class); + + $symfonyCommand->getName() + ->willReturn('agents'); + $symfonyCommand->getAliases() + ->willReturn([]); + $symfonyCommand->getDescription() + ->willReturn('Synchronize agents.'); + $symfonyCommand->getHelp() + ->willReturn(''); + $symfonyCommand->getDefinition() + ->willReturn($inputDefinition->reveal()); + $symfonyCommand->isHidden() + ->willReturn(false); $this->devTools->all() - ->willReturn([$symfonyCommand])->shouldBeCalledOnce(); - $this->devToolsComposer->all() - ->willReturn([$composerCommand])->shouldBeCalledOnce(); + ->willReturn([$symfonyCommand->reveal()]) + ->shouldBeCalledOnce(); $commands = $this->commandProvider->getCommands(); self::assertIsArray($commands); - self::assertCount(2, $commands); - self::assertSame($composerCommand, $commands[0]); - self::assertInstanceOf(ProxyCommand::class, $commands[1]); - self::assertSame('agents', $commands[1]->getName()); - } - - /** - * @return void - */ - #[Test] - public function getCommandsWillSkipLegacyReservedSymfonyAliases(): void - { - $legacyCommand = new class extends BaseCommand { - public function __construct() - { - parent::__construct('migrated'); - } - - public function getAliases(): array - { - return ['agents-alias']; - } - - protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int - { - return self::SUCCESS; - } - }; - $symfonyCommand = new class extends Command { - public function __construct() - { - parent::__construct('migrated'); - } - - protected function execute(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): int - { - return self::SUCCESS; - } - }; - - $this->devTools->all() - ->willReturn([$symfonyCommand])->shouldBeCalledOnce(); - $this->devToolsComposer->all() - ->willReturn([$legacyCommand])->shouldBeCalledOnce(); - - $commands = $this->commandProvider->getCommands(); - self::assertCount(1, $commands); - self::assertSame($legacyCommand, $commands[0]); + self::assertInstanceOf(ProxyCommand::class, $commands[0]); + self::assertSame('agents', $commands[0]->getName()); } } diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php index 214a3741a..8c6de41d3 100644 --- a/tests/Console/Command/AgentsCommandTest.php +++ b/tests/Console/Command/AgentsCommandTest.php @@ -91,8 +91,6 @@ public function executeWillFailWhenPackagedAgentsDirectoryDoesNotExist(): void $this->filesystem->exists($agentsPath) ->willReturn(false); - $this->synchronizer->setLogger($this->logger->reveal()) - ->shouldNotBeCalled(); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); $this->logger->info('Starting agents synchronization...') ->shouldBeCalledOnce(); @@ -124,8 +122,6 @@ public function executeWillCreateAgentsDirectoryWhenItDoesNotExist(): void ->willReturn(true, false); $this->filesystem->mkdir($agentsPath) ->shouldBeCalledOnce(); - $this->synchronizer->setLogger($this->logger->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); @@ -159,8 +155,6 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->filesystem->exists($agentsPath) ->willReturn(true, true); - $this->synchronizer->setLogger($this->logger->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index b809eb956..e57cb782c 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\CodeOwners\CodeOwnersGenerator; use FastForward\DevTools\Console\Command\CodeOwnersCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; @@ -93,7 +94,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->input->getOption('file') ->willReturn('.github/CODEOWNERS'); @@ -121,8 +122,8 @@ protected function setUp(): void $this->filesystem->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -278,7 +279,6 @@ public function executeWillPromptForOwnersWhenInteractiveInferenceFails(): void ->willReturn([]); $this->io->ask( 'No CODEOWNERS entries could be inferred from composer.json. Enter space-separated owners for "*" or leave blank to use a commented placeholder: ', - '', )->willReturn('php-fast-forward @mentordosnerds') ->shouldBeCalledOnce(); $this->generator->normalizeOwners('php-fast-forward @mentordosnerds') @@ -379,7 +379,7 @@ public function executeWillSkipReplacingExistingCodeOwnersWhenConfirmationIsDecl FileDiff::STATUS_CHANGED, 'Updating managed file ' . $targetPath . ' from generated CODEOWNERS content.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log( diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index 5cd06a132..14cfdc958 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\CopyResourceCommand; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -86,7 +87,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->output->writeln(Argument::any()); @@ -109,8 +110,8 @@ protected function setUp(): void $this->finderFactory->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -437,7 +438,7 @@ public function executeWillSkipReplacingDriftedFileWhenInteractiveConfirmationIs $this->fileDiffer->diff('/package/.editorconfig', '/project/.editorconfig') ->willReturn(new FileDiff(FileDiff::STATUS_CHANGED, 'Changed summary', "@@ -1 +1 @@\n-old\n+new")) ->shouldBeCalledOnce(); - $this->io->askConfirmation('Replace drifted resource /project/.editorconfig? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log('notice', 'Skipped replacing {target_path}.', Argument::type('array')) diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index c7810af63..12bfe5c38 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\FundingCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -91,7 +92,7 @@ protected function setUp(): void $this->processBuilder = $this->prophesize(ProcessBuilderInterface::class); $this->processQueue = $this->prophesize(ProcessQueueInterface::class); $this->normalizeProcess = $this->prophesize(Process::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->logger = $this->prophesize(LoggerInterface::class); $this->output->isDecorated() ->willReturn(false); @@ -133,8 +134,8 @@ protected function setUp(): void $this->processBuilder->reveal(), $this->processQueue->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -466,7 +467,7 @@ public function executeWillSkipComposerWriteWhenInteractiveConfirmationIsDecline ->willReturn(true); $this->input->isInteractive() ->willReturn(true); - $this->io->askConfirmation('Update managed file composer.json? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->filesystem->exists('composer.json') @@ -672,7 +673,7 @@ public function privateHelpersWillPromptAndNormalizeComposerFileArguments(): voi $shouldWriteManagedFile = new ReflectionMethod($this->command, 'shouldWriteManagedFile'); $normalizeComposerFile = new ReflectionMethod($this->command, 'normalizeComposerFile'); - $this->io->askConfirmation('Update managed file composer.alt.json? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(true) ->shouldBeCalledOnce(); $this->filesystem->dirname('composer.alt.json') diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index 7c186b350..c41dfc304 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -19,8 +19,9 @@ namespace FastForward\DevTools\Tests\Console\Command; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Prophecy\Argument; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\GitAttributesCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; @@ -135,7 +136,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->output->writeln(Argument::any()); @@ -168,8 +169,8 @@ protected function setUp(): void $this->filesystem->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -484,7 +485,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v FileDiff::STATUS_CHANGED, 'Managed file needs update.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $gitattributesPath), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->notice('Skipped updating {gitattributes_path}.', Argument::type('array')) diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index c450b9f6e..24a114c7c 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -19,8 +19,9 @@ namespace FastForward\DevTools\Tests\Console\Command; +use Symfony\Component\Console\Question\ConfirmationQuestion; use FastForward\DevTools\Resource\FileDiff; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\GitHooksCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FinderFactoryInterface; @@ -90,7 +91,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) @@ -114,8 +115,8 @@ protected function setUp(): void $this->finderFactory->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -309,7 +310,7 @@ public function executeWillSkipReplacingHookWhenInteractiveConfirmationIsDecline $this->fileDiffer->formatForConsole("@@ -1 +1 @@\n-old\n+new", false) ->willReturn("@@ -1 +1 @@\n-old\n+new") ->shouldBeCalledOnce(); - $this->io->askConfirmation('Replace drifted Git hook /app/.git/hooks/post-merge? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->notice( diff --git a/tests/Console/Command/GitIgnoreCommandTest.php b/tests/Console/Command/GitIgnoreCommandTest.php index f1787d48a..318fd2851 100644 --- a/tests/Console/Command/GitIgnoreCommandTest.php +++ b/tests/Console/Command/GitIgnoreCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\GitIgnoreCommand; use FastForward\DevTools\GitIgnore\GitIgnoreInterface; use FastForward\DevTools\GitIgnore\MergerInterface; @@ -124,7 +125,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) @@ -181,8 +182,8 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -299,7 +300,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v ->willReturn(true); $this->input->isInteractive() ->willReturn(true); - $this->io->askConfirmation(\sprintf('Update managed file %s? [y/N] ', self::TARGET_PATH), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log( diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index b14edd534..5596a79e2 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\LicenseCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Output\GithubActionOutput; @@ -98,7 +99,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->input->getOption('dry-run') @@ -121,8 +122,8 @@ protected function setUp(): void $this->filesystem->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -370,7 +371,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v FileDiff::STATUS_CHANGED, 'Updating managed file ' . $targetPath . ' from generated LICENSE content.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->notice( diff --git a/tests/Console/Command/SkillsCommandTest.php b/tests/Console/Command/SkillsCommandTest.php index 4825ee1ba..d2e72d79c 100644 --- a/tests/Console/Command/SkillsCommandTest.php +++ b/tests/Console/Command/SkillsCommandTest.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\Console\Application; -use Composer\IO\IOInterface; use FastForward\DevTools\Console\Command\SkillsCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -37,7 +35,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; use ReflectionMethod; -use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -62,10 +59,6 @@ final class SkillsCommandTest extends TestCase private ObjectProphecy $output; - private ObjectProphecy $application; - - private ObjectProphecy $io; - private SkillsCommand $command; /** @@ -78,13 +71,6 @@ protected function setUp(): void $this->logger = $this->prophesize(LoggerInterface::class); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); - $this->application = $this->prophesize(Application::class); - $this->io = $this->prophesize(IOInterface::class); - - $this->application->getHelperSet() - ->willReturn(new HelperSet()); - $this->application->getIO() - ->willReturn($this->io->reveal()); $this->filesystem->getAbsolutePath('.agents/skills') ->willReturn(getcwd() . '/.agents/skills'); @@ -93,7 +79,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->logger->reveal(), ); - $this->command->setApplication($this->application->reveal()); } /** @@ -106,7 +91,6 @@ public function executeWillFailWhenPackagedSkillsDirectoryDoesNotExist(): void $this->filesystem->exists($skillsPath) ->willReturn(false); - $this->synchronizer->setLogger(Argument::cetera())->shouldNotBeCalled(); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); $this->logger->info('Starting skills synchronization...') ->shouldBeCalledOnce(); @@ -138,8 +122,6 @@ public function executeWillCreateSkillsDirectoryWhenItDoesNotExist(): void ->willReturn(true, false); $this->filesystem->mkdir($skillsPath) ->shouldBeCalledOnce(); - $this->synchronizer->setLogger($this->io->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); @@ -173,8 +155,6 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->filesystem->exists($skillsPath) ->willReturn(true, true); - $this->synchronizer->setLogger($this->io->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index 85a09b560..19d5d0e25 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\UpdateComposerJsonCommand; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -78,7 +79,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) @@ -102,8 +103,8 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -432,7 +433,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v FileDiff::STATUS_CHANGED, 'composer.json must be updated.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation('Update managed file /app/composer.json? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log( diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index 486d82f2f..720efcf5f 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -21,6 +21,7 @@ use ArrayIterator; use FastForward\DevTools\Console\Command\AgentsCommand; +use FastForward\DevTools\Console\Command\SyncCommand; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use PHPUnit\Framework\Attributes\CoversClass; @@ -37,6 +38,7 @@ #[CoversClass(DevToolsCommandLoader::class)] #[UsesClass(AgentsCommand::class)] +#[UsesClass(SyncCommand::class)] final class DevToolsCommandLoaderTest extends TestCase { use ProphecyTrait; @@ -106,7 +108,7 @@ public function constructorWillRegisterOnlyInstantiableCommands(): void * @return void */ #[Test] - public function constructorWillSkipLegacyBaseCommands(): void + public function constructorWillRegisterCommandAliasesFromAsCommandAttribute(): void { $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; @@ -125,12 +127,15 @@ public function constructorWillSkipLegacyBaseCommands(): void ->shouldBeCalled(); $this->finder->getIterator() ->willReturn(new ArrayIterator([ - new SplFileInfo($commandDirectory . '/CodeStyleCommand.php', '', 'CodeStyleCommand.php'), + new SplFileInfo($commandDirectory . '/SyncCommand.php', '', 'SyncCommand.php'), ]))->shouldBeCalled(); + $this->container->has(SyncCommand::class)->willReturn(true)->shouldBeCalled(); $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); - self::assertFalse($loader->has('code-style')); + self::assertTrue($loader->has('synchronize')); + self::assertTrue($loader->has('dev-tools:sync')); + self::assertTrue($loader->has('sync')); } /** diff --git a/tests/Sync/PackagedDirectorySynchronizerTest.php b/tests/Sync/PackagedDirectorySynchronizerTest.php index c5db7bbea..222932dae 100644 --- a/tests/Sync/PackagedDirectorySynchronizerTest.php +++ b/tests/Sync/PackagedDirectorySynchronizerTest.php @@ -73,29 +73,6 @@ protected function setUp(): void $this->logger = $this->prophesize(LoggerInterface::class); } - /** - * @return void - */ - #[Test] - public function setLoggerWillReplaceTheActiveLogger(): void - { - $replacementLogger = $this->prophesize(LoggerInterface::class); - $synchronizer = $this->createSynchronizer(); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(false); - - $synchronizer->setLogger($replacementLogger->reveal()); - - $replacementLogger->error('No packaged .agents/agents found at: /package/.agents/agents') - ->shouldBeCalledOnce(); - - $result = $synchronizer - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertTrue($result->failed()); - } - /** * @return void */ @@ -347,21 +324,11 @@ private function createEntry(string $entryName, string $sourcePath, bool $isDire * @return PackagedDirectorySynchronizer */ private function createSynchronizer(): PackagedDirectorySynchronizer - { - return $this->createSynchronizerWithLogger($this->logger->reveal()); - } - - /** - * @param LoggerInterface $logger - * - * @return PackagedDirectorySynchronizer - */ - private function createSynchronizerWithLogger(LoggerInterface $logger): PackagedDirectorySynchronizer { return new PackagedDirectorySynchronizer( $this->filesystem->reveal(), $this->finderFactory->reveal(), - $logger, + $this->logger->reveal(), ); } } From 0d5e111b1a7654c296935c55916416d72295b05a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 06:06:44 +0000 Subject: [PATCH 04/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 3dea0d478..737f56f4f 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 3dea0d4783241698c7c8b275906f12f6e22d1212 +Subproject commit 737f56f4f94a6516426badf20052114c977b8809 From 07f1ac649b60c599277652995105f09147e9dc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sat, 25 Apr 2026 03:09:55 -0300 Subject: [PATCH 05/31] refactor: remove logger-aware command interface --- src/Console/Command/AgentsCommand.php | 7 ++-- src/Console/Command/ChangelogCheckCommand.php | 5 ++- src/Console/Command/ChangelogEntryCommand.php | 5 ++- .../Command/ChangelogNextVersionCommand.php | 5 ++- .../Command/ChangelogPromoteCommand.php | 5 ++- src/Console/Command/ChangelogShowCommand.php | 5 ++- src/Console/Command/CodeOwnersCommand.php | 5 ++- src/Console/Command/CodeStyleCommand.php | 5 ++- src/Console/Command/CopyResourceCommand.php | 5 ++- src/Console/Command/DependenciesCommand.php | 5 ++- src/Console/Command/DocsCommand.php | 5 ++- src/Console/Command/FundingCommand.php | 5 ++- src/Console/Command/GitAttributesCommand.php | 5 ++- src/Console/Command/GitHooksCommand.php | 5 ++- src/Console/Command/GitIgnoreCommand.php | 5 ++- src/Console/Command/LicenseCommand.php | 2 +- .../Command/LoggerAwareCommandInterface.php | 36 ------------------- src/Console/Command/MetricsCommand.php | 5 ++- src/Console/Command/PhpDocCommand.php | 5 ++- src/Console/Command/RefactorCommand.php | 5 ++- src/Console/Command/ReportsCommand.php | 5 ++- src/Console/Command/SkillsCommand.php | 5 ++- src/Console/Command/StandardsCommand.php | 5 ++- src/Console/Command/SyncCommand.php | 5 ++- src/Console/Command/TestsCommand.php | 5 ++- .../Command/Traits/HasCommandLogger.php | 18 +++++----- .../Command/UpdateComposerJsonCommand.php | 5 ++- src/Console/Command/WikiCommand.php | 5 ++- 28 files changed, 62 insertions(+), 121 deletions(-) delete mode 100644 src/Console/Command/LoggerAwareCommandInterface.php diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index 7ab7f9a9e..c864ae13b 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -34,9 +34,8 @@ * Synchronizes packaged Fast Forward project agents into the consumer repository. */ #[AsCommand(name: 'agents', description: 'Synchronizes Fast Forward project agents into .agents/agents directory.')] -final class AgentsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class AgentsCommand extends Command +{ use HasJsonOption; use LogsCommandResults; private const string AGENTS_DIRECTORY = '.agents/agents'; @@ -51,7 +50,7 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly LoggerInterface $logger, ) { - parent::__construct('agents'); + parent::__construct(); } /** diff --git a/src/Console/Command/ChangelogCheckCommand.php b/src/Console/Command/ChangelogCheckCommand.php index af9544699..f184d03b9 100644 --- a/src/Console/Command/ChangelogCheckCommand.php +++ b/src/Console/Command/ChangelogCheckCommand.php @@ -37,9 +37,8 @@ name: 'changelog:check', description: 'Checks whether a changelog file contains meaningful unreleased entries.' )] -final class ChangelogCheckCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class ChangelogCheckCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogEntryCommand.php b/src/Console/Command/ChangelogEntryCommand.php index 8a9a98763..d37c765e0 100644 --- a/src/Console/Command/ChangelogEntryCommand.php +++ b/src/Console/Command/ChangelogEntryCommand.php @@ -40,9 +40,8 @@ name: 'changelog:entry', description: 'Adds a changelog entry to Unreleased or a specific version section.' )] -final class ChangelogEntryCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class ChangelogEntryCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogNextVersionCommand.php b/src/Console/Command/ChangelogNextVersionCommand.php index a68879566..54ec3fadc 100644 --- a/src/Console/Command/ChangelogNextVersionCommand.php +++ b/src/Console/Command/ChangelogNextVersionCommand.php @@ -38,9 +38,8 @@ name: 'changelog:next-version', description: 'Infers the next semantic version from the Unreleased changelog section.' )] -final class ChangelogNextVersionCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class ChangelogNextVersionCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogPromoteCommand.php b/src/Console/Command/ChangelogPromoteCommand.php index 8012456f6..50d29bc17 100644 --- a/src/Console/Command/ChangelogPromoteCommand.php +++ b/src/Console/Command/ChangelogPromoteCommand.php @@ -40,9 +40,8 @@ name: 'changelog:promote', description: 'Promotes Unreleased entries into a published changelog version.' )] -final class ChangelogPromoteCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class ChangelogPromoteCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogShowCommand.php b/src/Console/Command/ChangelogShowCommand.php index 167a40f43..ccfa02f49 100644 --- a/src/Console/Command/ChangelogShowCommand.php +++ b/src/Console/Command/ChangelogShowCommand.php @@ -36,9 +36,8 @@ * Prints the rendered notes body for a released changelog version. */ #[AsCommand(name: 'changelog:show', description: 'Prints the notes body for a released changelog version.')] -final class ChangelogShowCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class ChangelogShowCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/CodeOwnersCommand.php b/src/Console/Command/CodeOwnersCommand.php index fe969624c..0be25b516 100644 --- a/src/Console/Command/CodeOwnersCommand.php +++ b/src/Console/Command/CodeOwnersCommand.php @@ -38,9 +38,8 @@ * Generates and synchronizes CODEOWNERS files from local project metadata. */ #[AsCommand(name: 'codeowners', description: 'Generates .github/CODEOWNERS from local project metadata.')] -final class CodeOwnersCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class CodeOwnersCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 1e9e3e7d6..2f9dd401c 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -40,9 +40,8 @@ name: 'code-style', description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.' )] -final class CodeStyleCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class CodeStyleCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 6862f1662..369e56d3a 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -40,9 +40,8 @@ * Copies packaged or local resources into the consumer repository. */ #[AsCommand(name: 'copy-resource', description: 'Copies a file or directory resource into the current project.')] -final class CopyResourceCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class CopyResourceCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index 2cbcb46e4..c1c54d29d 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -47,9 +47,8 @@ description: 'Analyzes missing, unused, misplaced, and outdated Composer dependencies.', aliases: ['deps'] )] -final class DependenciesCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class DependenciesCommand extends Command +{ use HasJsonOption; use LogsCommandResults; private const string ANALYSER_CONFIG = 'composer-dependency-analyser.php'; diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index ddba461c7..8e2386091 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -47,9 +47,8 @@ * command surface. */ #[AsCommand(name: 'docs', description: 'Generates API documentation.')] -final class DocsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasCacheOption; +final class DocsCommand extends Command +{ use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index dbed08075..cd244f9be 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -44,9 +44,8 @@ name: 'funding', description: 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.' )] -final class FundingCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class FundingCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index 7120676c2..f09351c2b 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -51,9 +51,8 @@ name: 'gitattributes', description: 'Manages .gitattributes export-ignore rules for leaner package archives.' )] -final class GitAttributesCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class GitAttributesCommand extends Command +{ use HasJsonOption; use LogsCommandResults; private const string FILENAME = '.gitattributes'; diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index 2abf9ce60..b744f61aa 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -40,9 +40,8 @@ * Installs packaged Git hooks for the consumer repository. */ #[AsCommand(name: 'git-hooks', description: 'Installs Fast Forward Git hooks.')] -final class GitHooksCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class GitHooksCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index 0a61a3276..66db7a1a4 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -47,9 +47,8 @@ * to the canonical and project .gitignore files respectively. */ #[AsCommand(name: 'gitignore', description: 'Merges and synchronizes .gitignore files.')] -final class GitIgnoreCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class GitIgnoreCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index 1c88e02d8..33f4f80d1 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -40,7 +40,7 @@ * license is declared in composer.json. */ #[AsCommand(name: 'license', description: 'Generates a LICENSE file from composer.json license information.')] -final class LicenseCommand extends Command implements LoggerAwareCommandInterface +final class LicenseCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/LoggerAwareCommandInterface.php b/src/Console/Command/LoggerAwareCommandInterface.php deleted file mode 100644 index 9010f8385..000000000 --- a/src/Console/Command/LoggerAwareCommandInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Console\Command; - -use Psr\Log\LoggerInterface; - -/** - * Defines the logger contract consumed by reusable command result helpers. - * - * Commands that compose shared logging traits expose their logger through this - * interface so the traits can stay agnostic of the concrete command class. - */ -interface LoggerAwareCommandInterface -{ - /** - * Returns the logger used to emit command lifecycle messages. - */ - public function getLogger(): LoggerInterface; -} diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index bceef3aa9..0594955e3 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -35,9 +35,8 @@ use function rtrim; #[AsCommand(name: 'metrics', description: 'Analyzes code metrics with PhpMetrics.')] -final class MetricsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class MetricsCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 46a422bbc..dda969704 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -45,9 +45,8 @@ * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ #[AsCommand(name: 'phpdoc', description: 'Checks and fixes PHPDocs.')] -final class PhpDocCommand extends Command implements LoggerAwareCommandInterface -{ - use HasCacheOption; +final class PhpDocCommand extends Command +{ use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index af5dbf8b8..31350aa7c 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -37,9 +37,8 @@ * This class MUST NOT be extended and SHALL encapsulate the logic for Rector invocation. */ #[AsCommand(name: 'refactor', description: 'Runs Rector for code refactoring.', aliases: ['rector'])] -final class RefactorCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class RefactorCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index 32c8dcef4..049879747 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -39,9 +39,8 @@ * This class MUST NOT be overridden and SHALL securely combine docs and testing commands. */ #[AsCommand(name: 'reports', description: 'Generates the frontpage for Fast Forward documentation.')] -final class ReportsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasCacheOption; +final class ReportsCommand extends Command +{ use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php index 5a10a249e..a3a9f3762 100644 --- a/src/Console/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -45,9 +45,8 @@ * into Symfony Console output and process exit codes. */ #[AsCommand(name: 'skills', description: 'Synchronizes Fast Forward skills into .agents/skills directory.')] -final class SkillsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class SkillsCommand extends Command +{ use HasJsonOption; use LogsCommandResults; private const string SKILLS_DIRECTORY = '.agents/skills'; diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index 7093f9fb4..cbc520780 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -39,9 +39,8 @@ * This class MUST NOT be modified through inheritance and SHALL streamline code validation workflows. */ #[AsCommand(name: 'standards', description: 'Runs Fast Forward code standards checks.')] -final class StandardsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasCacheOption; +final class StandardsCommand extends Command +{ use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index 644bd444c..5b67df890 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -40,9 +40,8 @@ description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, CODEOWNERS, .editorconfig, and .gitattributes in the root project.', aliases: ['dev-tools:sync', 'sync'], )] -final class SyncCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class SyncCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 375cf6e1e..73c68a29d 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -47,9 +47,8 @@ * This class MUST NOT be overridden and SHALL configure testing parameters dynamically. */ #[AsCommand(name: 'tests', description: 'Runs PHPUnit tests.')] -final class TestsCommand extends Command implements LoggerAwareCommandInterface -{ - use HasCacheOption; +final class TestsCommand extends Command +{ use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/Traits/HasCommandLogger.php b/src/Console/Command/Traits/HasCommandLogger.php index 5bbcd0c2f..e4e21c8bb 100644 --- a/src/Console/Command/Traits/HasCommandLogger.php +++ b/src/Console/Command/Traits/HasCommandLogger.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command\Traits; -use FastForward\DevTools\Console\Command\LoggerAwareCommandInterface; use LogicException; use Psr\Log\LoggerInterface; @@ -39,15 +38,18 @@ trait HasCommandLogger */ public function getLogger(): LoggerInterface { - if ( - ! $this instanceof LoggerAwareCommandInterface - || (! property_exists($this, 'logger') || null === $this->logger) - || ! $this->logger instanceof LoggerInterface - ) { + if (! property_exists($this, 'logger') || null === $this->logger) { throw new LogicException(\sprintf( - 'Commands using %s MUST implement %s and expose an initialized $logger property with an instance of %s.', + 'Commands using %s MUST expose an initialized $logger property with an instance of %s.', + LogsCommandResults::class, + LoggerInterface::class, + )); + } + + if (! $this->logger instanceof LoggerInterface) { + throw new LogicException(\sprintf( + 'Commands using %s MUST expose a %s instance on the $logger property.', LogsCommandResults::class, - LoggerAwareCommandInterface::class, LoggerInterface::class, )); } diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index 6f30b4ff5..f20cfa68e 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -48,9 +48,8 @@ name: 'update-composer-json', description: 'Updates composer.json with Fast Forward dev-tools scripts and metadata.' )] -final class UpdateComposerJsonCommand extends Command implements LoggerAwareCommandInterface -{ - use HasJsonOption; +final class UpdateComposerJsonCommand extends Command +{ use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 81c549a44..e5416c1cf 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -44,9 +44,8 @@ * This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task. */ #[AsCommand(name: 'wiki', description: 'Generates API documentation in Markdown format.')] -final class WikiCommand extends Command implements LoggerAwareCommandInterface -{ - use HasCacheOption; +final class WikiCommand extends Command +{ use HasCacheOption; use HasJsonOption; use LogsCommandResults; From 7994ab17e75f37e7be1a15b1a8c80ac629ac2301 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 06:10:57 +0000 Subject: [PATCH 06/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 737f56f4f..bbb6338bd 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 737f56f4f94a6516426badf20052114c977b8809 +Subproject commit bbb6338bd0c1bb51571a3c102ae5ce21421e5138 From a7390f609f1096491f81c5cd4b7f88a293c5651a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 04:00:54 -0300 Subject: [PATCH 07/31] feat: change dirname e basename method names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- composer.json | 4 +++- src/Filesystem/Filesystem.php | 4 ++-- src/Filesystem/FilesystemInterface.php | 4 ++-- ...lorPreservingProcessEnvironmentConfigurator.php | 1 + src/Sync/PackagedDirectorySynchronizer.php | 8 ++++---- .../Checker/UnreleasedEntryCheckerTest.php | 2 +- tests/Changelog/Manager/ChangelogManagerTest.php | 2 +- tests/Console/Command/CodeOwnersCommandTest.php | 12 ++++++------ tests/Console/Command/FundingCommandTest.php | 14 +++++++------- tests/Console/Command/GitHooksCommandTest.php | 2 +- tests/Filesystem/FilesystemTest.php | 8 ++++---- tests/Sync/PackagedDirectorySynchronizerTest.php | 6 +++--- 12 files changed, 35 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 0362b1ec7..942bd8aa7 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,6 @@ "require": { "php": "^8.3", "composer-plugin-api": "^2.0", - "composer/composer": "^2.9", "container-interop/service-provider": "^0.4.1", "dg/bypass-finals": "^1.9", "ergebnis/agent-detector": "^1.1", @@ -82,6 +81,9 @@ "thecodingmachine/safe": "^3.4", "twig/twig": "^3.24" }, + "require-dev": { + "composer/composer": "^2.9" + }, "minimum-stability": "stable", "autoload": { "psr-4": { diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index f96217f31..8f22a2bb9 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -197,7 +197,7 @@ public function makePathRelative(string $path, ?string $basePath = null): string * * @return string the base name of the given path */ - public function basename(string $path, string $suffix = ''): string + public function getBasename(string $path, string $suffix = ''): string { return basename($path, $suffix); } @@ -210,7 +210,7 @@ public function basename(string $path, string $suffix = ''): string * * @return string the parent path name */ - public function dirname(string $path, int $levels = 1): string + public function getDirectory(string $path, int $levels = 1): string { return \dirname($path, $levels); } diff --git a/src/Filesystem/FilesystemInterface.php b/src/Filesystem/FilesystemInterface.php index a58d6b83e..74a213521 100644 --- a/src/Filesystem/FilesystemInterface.php +++ b/src/Filesystem/FilesystemInterface.php @@ -140,7 +140,7 @@ public function makePathRelative(string $path, ?string $basePath = null): string * * @return string the base name of the given path */ - public function basename(string $path, string $suffix = ''): string; + public function getBasename(string $path, string $suffix = ''): string; /** * Returns a parent directory's path. @@ -150,5 +150,5 @@ public function basename(string $path, string $suffix = ''): string; * * @return string the parent path name */ - public function dirname(string $path, int $levels = 1): string; + public function getDirectory(string $path, int $levels = 1): string; } diff --git a/src/Process/ColorPreservingProcessEnvironmentConfigurator.php b/src/Process/ColorPreservingProcessEnvironmentConfigurator.php index ca833672c..a93d02ba5 100644 --- a/src/Process/ColorPreservingProcessEnvironmentConfigurator.php +++ b/src/Process/ColorPreservingProcessEnvironmentConfigurator.php @@ -81,6 +81,7 @@ private function shouldForceColor(OutputInterface $output): bool if ($this->outputCapabilityDetector->supportsAnsi($output)) { return true; } + if ($this->isTruthyEnvironmentFlag('FORCE_COLOR')) { return true; } diff --git a/src/Sync/PackagedDirectorySynchronizer.php b/src/Sync/PackagedDirectorySynchronizer.php index 2728c6c61..733af80fc 100644 --- a/src/Sync/PackagedDirectorySynchronizer.php +++ b/src/Sync/PackagedDirectorySynchronizer.php @@ -27,7 +27,7 @@ /** * Synchronizes one packaged directory of symlinked entries into a consumer repository. */ -final class PackagedDirectorySynchronizer +final readonly class PackagedDirectorySynchronizer { /** * Initializes the synchronizer with a filesystem and finder factory. @@ -37,8 +37,8 @@ final class PackagedDirectorySynchronizer * @param LoggerInterface $logger Logger for recording synchronization actions and decisions */ public function __construct( - private readonly FilesystemInterface $filesystem, - private readonly FinderFactoryInterface $finderFactory, + private FilesystemInterface $filesystem, + private FinderFactoryInterface $finderFactory, private LoggerInterface $logger, ) {} @@ -132,7 +132,7 @@ private function createNewLink( SynchronizeResult $result, ): void { $relativeSourcePath = $this->normalizeRelativeSourcePath( - $this->filesystem->makePathRelative($sourcePath, $this->filesystem->dirname($targetLink)), + $this->filesystem->makePathRelative($sourcePath, $this->filesystem->getDirectory($targetLink)), $isDirectory, ); diff --git a/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php b/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php index ed8b066c4..aeac837e7 100644 --- a/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php +++ b/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php @@ -71,7 +71,7 @@ protected function setUp(): void $this->gitClient = $this->prophesize(GitClientInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->parser = $this->prophesize(ChangelogParserInterface::class); - $this->filesystem->dirname(self::FILE) + $this->filesystem->getDirectory(self::FILE) ->willReturn(self::WORKING_DIRECTORY); $this->checker = new UnreleasedEntryChecker( $this->filesystem->reveal(), diff --git a/tests/Changelog/Manager/ChangelogManagerTest.php b/tests/Changelog/Manager/ChangelogManagerTest.php index 3ec95bff3..e6527031a 100644 --- a/tests/Changelog/Manager/ChangelogManagerTest.php +++ b/tests/Changelog/Manager/ChangelogManagerTest.php @@ -85,7 +85,7 @@ protected function setUp(): void $this->renderer->reveal(), $this->gitClient->reveal(), ); - $this->filesystem->dirname(self::FILE) + $this->filesystem->getDirectory(self::FILE) ->willReturn(self::WORKING_DIRECTORY); } diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index e57cb782c..673b3af2e 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -155,7 +155,7 @@ public function executeWillWriteGeneratedCodeOwnersWhenFileIsMissing(): void $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn($targetDirectory); $this->filesystem->exists($targetPath) ->willReturn(false, false); @@ -193,7 +193,7 @@ public function executeWillSkipExistingCodeOwnersByDefault(): void $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -227,7 +227,7 @@ public function executeWillFailCheckModeWhenDriftIsDetected(): void ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -269,7 +269,7 @@ public function executeWillPromptForOwnersWhenInteractiveInferenceFails(): void ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn($targetDirectory); $this->filesystem->exists($targetPath) ->willReturn(false, false); @@ -318,7 +318,7 @@ public function executeWillReturnSuccessOnDryRunWhenDriftIsDetected(): void ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -359,7 +359,7 @@ public function executeWillSkipReplacingExistingCodeOwnersWhenConfirmationIsDecl ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index 12bfe5c38..32335487e 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -112,11 +112,11 @@ protected function setUp(): void ->willReturn(false); $this->input->getOption('interactive') ->willReturn(false); - $this->filesystem->dirname('.github/FUNDING.yml') + $this->filesystem->getDirectory('.github/FUNDING.yml') ->willReturn('.github'); - $this->filesystem->dirname('composer.json') + $this->filesystem->getDirectory('composer.json') ->willReturn('.'); - $this->filesystem->basename('composer.json') + $this->filesystem->getBasename('composer.json') ->willReturn('composer.json'); $this->processBuilder->withArgument(Argument::any())->willReturn($this->processBuilder->reveal()); $this->processBuilder->withArgument(Argument::any(), Argument::any())->willReturn( @@ -573,9 +573,9 @@ public function executeWillPassWorkingDirectoryAndAlternateManifestToComposerNor ->willReturn(true); $this->filesystem->readFile('.github/FUNDING.yml') ->willReturn($fundingYaml); - $this->filesystem->dirname($composerFile) + $this->filesystem->getDirectory($composerFile) ->willReturn('build/custom'); - $this->filesystem->basename($composerFile) + $this->filesystem->getBasename($composerFile) ->willReturn('composer.alt.json'); $this->processBuilder->withArgument('--working-dir', 'build/custom') ->willReturn($this->processBuilder->reveal()) @@ -676,9 +676,9 @@ public function privateHelpersWillPromptAndNormalizeComposerFileArguments(): voi $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(true) ->shouldBeCalledOnce(); - $this->filesystem->dirname('composer.alt.json') + $this->filesystem->getDirectory('composer.alt.json') ->willReturn('.'); - $this->filesystem->basename('composer.alt.json') + $this->filesystem->getBasename('composer.alt.json') ->willReturn('composer.alt.json'); $this->processBuilder->withArgument('--file', 'composer.alt.json') ->willReturn($this->processBuilder->reveal()) diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index 24a114c7c..84b37d609 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -400,7 +400,7 @@ public function executeWillReportInstallFailureWhenReplacementStillCannotBeWritt new IOException('Target file could not be opened for writing.', 0, null, '/app/.git/hooks/post-merge') ) ->shouldBeCalledOnce(); - $this->filesystem->basename('/app/.git/hooks/post-merge') + $this->filesystem->getBasename('/app/.git/hooks/post-merge') ->willReturn('post-merge') ->shouldBeCalledOnce(); $this->logger->error( diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index 7f315cd8e..70bebd409 100644 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -114,8 +114,8 @@ public function getAbsolutePathWillResolveRelativeBasePathsAgainstCurrentWorking #[Test] public function basenameWillReturnCorrectBasename(): void { - self::assertSame('file', $this->filesystem->basename('/path/to/file.txt', '.txt')); - self::assertSame('file.txt', $this->filesystem->basename('/path/to/file.txt')); + self::assertSame('file', $this->filesystem->getBasename('/path/to/file.txt', '.txt')); + self::assertSame('file.txt', $this->filesystem->getBasename('/path/to/file.txt')); } /** @@ -124,8 +124,8 @@ public function basenameWillReturnCorrectBasename(): void #[Test] public function dirnameWillReturnCorrectDirname(): void { - self::assertSame('/path/to', $this->filesystem->dirname('/path/to/file.txt')); - self::assertSame('/path', $this->filesystem->dirname('/path/to/file.txt', 2)); + self::assertSame('/path/to', $this->filesystem->getDirectory('/path/to/file.txt')); + self::assertSame('/path', $this->filesystem->getDirectory('/path/to/file.txt', 2)); } /** diff --git a/tests/Sync/PackagedDirectorySynchronizerTest.php b/tests/Sync/PackagedDirectorySynchronizerTest.php index 222932dae..bd6c11cee 100644 --- a/tests/Sync/PackagedDirectorySynchronizerTest.php +++ b/tests/Sync/PackagedDirectorySynchronizerTest.php @@ -112,7 +112,7 @@ public function synchronizeWithMissingTargetDirWillCreateItAndCreateLinks(): voi ->shouldBeCalledOnce(); $this->filesystem->exists('/consumer/.agents/agents/issue-editor') ->willReturn(false); - $this->filesystem->dirname('/consumer/.agents/agents/issue-editor') + $this->filesystem->getDirectory('/consumer/.agents/agents/issue-editor') ->willReturn('/consumer/.agents/agents') ->shouldBeCalledOnce(); $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') @@ -190,7 +190,7 @@ public function synchronizeWillRepairBrokenSymlink(): void ->willReturn(false); $this->filesystem->remove($targetLink) ->shouldBeCalledOnce(); - $this->filesystem->dirname($targetLink) + $this->filesystem->getDirectory($targetLink) ->willReturn('/consumer/.agents/agents') ->shouldBeCalledOnce(); $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') @@ -260,7 +260,7 @@ public function synchronizeWillCreateLinksForTopLevelFiles(): void ->willReturn(true); $this->filesystem->exists($targetLink) ->willReturn(false); - $this->filesystem->dirname($targetLink) + $this->filesystem->getDirectory($targetLink) ->willReturn('/consumer/.agents/agents') ->shouldBeCalledOnce(); $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') From 565cd2af8a1a3ac48cfcf6e7aaa248874cffb2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 04:01:25 -0300 Subject: [PATCH 08/31] feat: change dirname e basename method names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Changelog/Checker/UnreleasedEntryChecker.php | 2 +- src/Changelog/Manager/ChangelogManager.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Changelog/Checker/UnreleasedEntryChecker.php b/src/Changelog/Checker/UnreleasedEntryChecker.php index b9fb9b289..2aaafdc26 100644 --- a/src/Changelog/Checker/UnreleasedEntryChecker.php +++ b/src/Changelog/Checker/UnreleasedEntryChecker.php @@ -74,7 +74,7 @@ public function hasPendingChanges(string $file, ?string $againstReference = null } try { - $baseline = $this->gitClient->show($againstReference, $file, $this->filesystem->dirname($file)); + $baseline = $this->gitClient->show($againstReference, $file, $this->filesystem->getDirectory($file)); } catch (Throwable) { return true; } diff --git a/src/Changelog/Manager/ChangelogManager.php b/src/Changelog/Manager/ChangelogManager.php index 8372f0518..72b3e0135 100644 --- a/src/Changelog/Manager/ChangelogManager.php +++ b/src/Changelog/Manager/ChangelogManager.php @@ -189,7 +189,7 @@ private function persist(string $file, ChangelogDocument $document): void { $this->filesystem->dumpFile( $file, - $this->renderer->render($document, $this->resolveRepositoryUrl($this->filesystem->dirname($file))), + $this->renderer->render($document, $this->resolveRepositoryUrl($this->filesystem->getDirectory($file))), ); } } From 52b82310230670fce8c566b8a689355302d93b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 04:29:33 -0300 Subject: [PATCH 09/31] Fix coverage metadata and stabilize dev-tools command tests --- .../Capability/DevToolsCommandProvider.php | 7 ++++- src/Console/Command/AgentsCommand.php | 9 +++++-- src/Console/Command/ChangelogCheckCommand.php | 3 ++- src/Console/Command/ChangelogEntryCommand.php | 3 ++- .../Command/ChangelogNextVersionCommand.php | 3 ++- .../Command/ChangelogPromoteCommand.php | 3 ++- src/Console/Command/ChangelogShowCommand.php | 3 ++- src/Console/Command/CodeOwnersCommand.php | 11 +++++--- src/Console/Command/CodeStyleCommand.php | 8 +++--- src/Console/Command/CopyResourceCommand.php | 9 +++++-- src/Console/Command/DependenciesCommand.php | 7 ++--- src/Console/Command/DocsCommand.php | 9 +++++-- src/Console/Command/FundingCommand.php | 14 +++++----- src/Console/Command/GitAttributesCommand.php | 8 +++--- src/Console/Command/GitHooksCommand.php | 11 +++++--- src/Console/Command/GitIgnoreCommand.php | 9 +++++-- src/Console/Command/LicenseCommand.php | 6 ++++- src/Console/Command/MetricsCommand.php | 9 +++++-- src/Console/Command/PhpDocCommand.php | 5 ++-- src/Console/Command/RefactorCommand.php | 9 +++++-- src/Console/Command/ReportsCommand.php | 9 +++++-- src/Console/Command/SkillsCommand.php | 11 +++++--- src/Console/Command/StandardsCommand.php | 9 +++++-- src/Console/Command/SyncCommand.php | 7 ++--- src/Console/Command/TestsCommand.php | 5 ++-- .../Command/UpdateComposerJsonCommand.php | 8 +++--- src/Console/Command/WikiCommand.php | 9 +++++-- src/Console/DevTools.php | 20 ++++++++++++++ .../Output/OutputCapabilityDetector.php | 9 ++++++- .../DevToolsCommandProviderTest.php | 26 ++++++------------- tests/Composer/Json/ComposerJsonTest.php | 2 ++ .../Console/Command/CodeOwnersCommandTest.php | 2 +- .../Command/CopyResourceCommandTest.php | 2 +- tests/Console/Command/FundingCommandTest.php | 2 +- .../Command/GitAttributesCommandTest.php | 2 +- tests/Console/Command/GitHooksCommandTest.php | 2 +- .../Console/Command/GitIgnoreCommandTest.php | 2 +- tests/Console/Command/LicenseCommandTest.php | 2 +- tests/Console/Command/ReportsCommandTest.php | 2 ++ .../Console/Command/StandardsCommandTest.php | 2 ++ .../Command/UpdateComposerJsonCommandTest.php | 2 +- .../DevToolsCommandLoaderTest.php | 1 - .../composer-plugin-consumer/.gitignore | 11 +++++++- .../composer-plugin-consumer/composer.json | 2 +- 44 files changed, 206 insertions(+), 89 deletions(-) diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index 0793ecbfa..c23468ffd 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -31,6 +31,8 @@ */ final class DevToolsCommandProvider implements CommandProvider { + private const string COMMAND_NAMESPACE = 'FastForward\\DevTools\\Console\\Command\\'; + /** * {@inheritDoc} */ @@ -38,7 +40,10 @@ public function getCommands() { return array_map( static fn(Command $command): BaseCommand => new ProxyCommand($command), - DevTools::create()->all(), + array_filter( + DevTools::create()->all(), + static fn(Command $command): bool => str_starts_with($command::class, self::COMMAND_NAMESPACE), + ), ); } } diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index c864ae13b..d26595ac3 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -33,9 +33,14 @@ /** * Synchronizes packaged Fast Forward project agents into the consumer repository. */ -#[AsCommand(name: 'agents', description: 'Synchronizes Fast Forward project agents into .agents/agents directory.')] +#[AsCommand( + name: 'agents:agents', + description: 'Synchronizes Fast Forward project agents into .agents/agents directory.', + aliases: ['agents'], +)] final class AgentsCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; private const string AGENTS_DIRECTORY = '.agents/agents'; diff --git a/src/Console/Command/ChangelogCheckCommand.php b/src/Console/Command/ChangelogCheckCommand.php index f184d03b9..eaebc859b 100644 --- a/src/Console/Command/ChangelogCheckCommand.php +++ b/src/Console/Command/ChangelogCheckCommand.php @@ -38,7 +38,8 @@ description: 'Checks whether a changelog file contains meaningful unreleased entries.' )] final class ChangelogCheckCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogEntryCommand.php b/src/Console/Command/ChangelogEntryCommand.php index d37c765e0..7de3a385f 100644 --- a/src/Console/Command/ChangelogEntryCommand.php +++ b/src/Console/Command/ChangelogEntryCommand.php @@ -41,7 +41,8 @@ description: 'Adds a changelog entry to Unreleased or a specific version section.' )] final class ChangelogEntryCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogNextVersionCommand.php b/src/Console/Command/ChangelogNextVersionCommand.php index 54ec3fadc..18b940070 100644 --- a/src/Console/Command/ChangelogNextVersionCommand.php +++ b/src/Console/Command/ChangelogNextVersionCommand.php @@ -39,7 +39,8 @@ description: 'Infers the next semantic version from the Unreleased changelog section.' )] final class ChangelogNextVersionCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogPromoteCommand.php b/src/Console/Command/ChangelogPromoteCommand.php index 50d29bc17..6425f0715 100644 --- a/src/Console/Command/ChangelogPromoteCommand.php +++ b/src/Console/Command/ChangelogPromoteCommand.php @@ -41,7 +41,8 @@ description: 'Promotes Unreleased entries into a published changelog version.' )] final class ChangelogPromoteCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ChangelogShowCommand.php b/src/Console/Command/ChangelogShowCommand.php index ccfa02f49..dbc54c59e 100644 --- a/src/Console/Command/ChangelogShowCommand.php +++ b/src/Console/Command/ChangelogShowCommand.php @@ -37,7 +37,8 @@ */ #[AsCommand(name: 'changelog:show', description: 'Prints the notes body for a released changelog version.')] final class ChangelogShowCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/CodeOwnersCommand.php b/src/Console/Command/CodeOwnersCommand.php index 0be25b516..bf46812af 100644 --- a/src/Console/Command/CodeOwnersCommand.php +++ b/src/Console/Command/CodeOwnersCommand.php @@ -37,9 +37,14 @@ /** * Generates and synchronizes CODEOWNERS files from local project metadata. */ -#[AsCommand(name: 'codeowners', description: 'Generates .github/CODEOWNERS from local project metadata.')] +#[AsCommand( + name: 'github:codeowners', + description: 'Generates .github/CODEOWNERS from local project metadata.', + aliases: ['.github/CODEOWNERS', 'codeowners'], +)] final class CodeOwnersCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** @@ -112,7 +117,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $targetPath = $this->filesystem->getAbsolutePath((string) $input->getOption('file')); - $targetDirectory = $this->filesystem->dirname($targetPath); + $targetDirectory = $this->filesystem->getDirectory($targetPath); $overwrite = (bool) $input->getOption('overwrite'); $dryRun = (bool) $input->getOption('dry-run'); $check = (bool) $input->getOption('check'); diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 2f9dd401c..d9b35f18b 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -37,11 +37,13 @@ * This class MUST NOT be overridden and SHALL rely on external tools like ECS and Composer Normalize. */ #[AsCommand( - name: 'code-style', - description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.' + name: 'standards:code-style', + description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.', + aliases: ['code-style'] )] final class CodeStyleCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 369e56d3a..18e9cd287 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -39,9 +39,14 @@ /** * Copies packaged or local resources into the consumer repository. */ -#[AsCommand(name: 'copy-resource', description: 'Copies a file or directory resource into the current project.')] +#[AsCommand( + name: 'dev-tools:sync:copy', + description: 'Copies a file or directory resource into the current project.', + aliases: ['copy-resource'] +)] final class CopyResourceCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index c1c54d29d..0d2c5b117 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -43,12 +43,13 @@ * deterministic report that is friendly for local development and CI runs. */ #[AsCommand( - name: 'dependencies', + name: 'dev-tools:deps', description: 'Analyzes missing, unused, misplaced, and outdated Composer dependencies.', - aliases: ['deps'] + aliases: ['deps', 'dependencies'] )] final class DependenciesCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; private const string ANALYSER_CONFIG = 'composer-dependency-analyser.php'; diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index 8e2386091..eb3d8345d 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -46,9 +46,14 @@ * queue so logging and grouped output stay consistent with the rest of the * command surface. */ -#[AsCommand(name: 'docs', description: 'Generates API documentation.')] +#[AsCommand( + name: 'reports:docs', + description: 'Generates API documentation.', + aliases: ['reports:phpdoc', 'phpdoc', 'phpDocumentor', 'docs'], +)] final class DocsCommand extends Command -{ use HasCacheOption; +{ + use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index cd244f9be..f4e4e935f 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -41,11 +41,13 @@ * Synchronizes funding metadata between composer.json and .github/FUNDING.yml. */ #[AsCommand( - name: 'funding', - description: 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.' + name: 'github:funding', + description: 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.', + aliases: ['.github/FUNDING.yml', 'composer:funding', 'funding'], )] final class FundingCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** @@ -451,7 +453,7 @@ private function handleFundingFile( ); } - $this->filesystem->mkdir($this->filesystem->dirname($fundingFile)); + $this->filesystem->mkdir($this->filesystem->getDirectory($fundingFile)); $this->filesystem->dumpFile($fundingFile, $updatedFundingContents); return $this->success( @@ -491,13 +493,13 @@ private function normalizeComposerFile(string $composerFile, OutputInterface $ou ->withArgument('--ansi') ->withArgument('--no-update-lock'); - $workingDirectory = $this->filesystem->dirname($composerFile); + $workingDirectory = $this->filesystem->getDirectory($composerFile); if ('.' !== $workingDirectory) { $processBuilder = $processBuilder->withArgument('--working-dir', $workingDirectory); } - $composerBasename = $this->filesystem->basename($composerFile); + $composerBasename = $this->filesystem->getBasename($composerFile); if ('composer.json' !== $composerBasename) { $processBuilder = $processBuilder->withArgument('--file', $composerBasename); diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index f09351c2b..f453f0fb0 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -48,11 +48,13 @@ * to keep them out of Composer package archives. */ #[AsCommand( - name: 'gitattributes', - description: 'Manages .gitattributes export-ignore rules for leaner package archives.' + name: 'git:attributes', + description: 'Manages .gitattributes export-ignore rules for leaner package archives.', + aliases: ['.gitattributes', 'gitattributes'], )] final class GitAttributesCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; private const string FILENAME = '.gitattributes'; diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index b744f61aa..3dd87cbf8 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -39,9 +39,14 @@ /** * Installs packaged Git hooks for the consumer repository. */ -#[AsCommand(name: 'git-hooks', description: 'Installs Fast Forward Git hooks.')] +#[AsCommand( + name: 'git:hooks', + description: 'Installs Fast Forward Git hooks.', + aliases: ['.git/hooks', 'git-hooks'], +)] final class GitHooksCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** @@ -300,7 +305,7 @@ private function installHook( 'Failed to install {hook_name} hook automatically. Remove or unlock {hook_path} and rerun git-hooks.', [ 'input' => $input, - 'hook_name' => $this->filesystem->basename($hookPath), + 'hook_name' => $this->filesystem->getBasename($hookPath), 'hook_path' => $hookPath, 'error' => $ioException->getMessage(), 'file' => $ioException->getPath() ?? $hookPath, diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index 66db7a1a4..bc4971fb8 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -46,9 +46,14 @@ * The command accepts two options: --source and --target to specify the paths * to the canonical and project .gitignore files respectively. */ -#[AsCommand(name: 'gitignore', description: 'Merges and synchronizes .gitignore files.')] +#[AsCommand( + name: 'git:ignore', + description: 'Merges and synchronizes .gitignore files.', + aliases: ['.gitignore', 'gitignore'], +)] final class GitIgnoreCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index 33f4f80d1..beb0342e1 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -39,7 +39,11 @@ * This command generates a LICENSE file if one does not exist and a supported * license is declared in composer.json. */ -#[AsCommand(name: 'license', description: 'Generates a LICENSE file from composer.json license information.')] +#[AsCommand( + name: 'license:generate', + description: 'Generates a LICENSE file from composer.json license information.', + aliases: ['LICENSE.md', 'license'], +)] final class LicenseCommand extends Command { use HasJsonOption; diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 0594955e3..0758a7fca 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -34,9 +34,14 @@ use function rtrim; -#[AsCommand(name: 'metrics', description: 'Analyzes code metrics with PhpMetrics.')] +#[AsCommand( + name: 'reports:metrics', + description: 'Analyzes code metrics with PhpMetrics.', + aliases: ['reports:phpmetrics', 'phpmetrics', 'metrics'], +)] final class MetricsCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index dda969704..0482cdb8c 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -44,9 +44,10 @@ * Provides operations to inspect, lint, and repair PHPDoc comments across the project. * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ -#[AsCommand(name: 'phpdoc', description: 'Checks and fixes PHPDocs.')] +#[AsCommand(name: 'standards:phpdoc', description: 'Checks and fixes PHPDocs.', aliases: ['phpdoc'])] final class PhpDocCommand extends Command -{ use HasCacheOption; +{ + use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index 31350aa7c..6f2835104 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -36,9 +36,14 @@ * Provides functionality to execute automated code refactoring using Rector. * This class MUST NOT be extended and SHALL encapsulate the logic for Rector invocation. */ -#[AsCommand(name: 'refactor', description: 'Runs Rector for code refactoring.', aliases: ['rector'])] +#[AsCommand( + name: 'standards:rector', + description: 'Runs Rector for code refactoring.', + aliases: ['refactor', 'rector'] +)] final class RefactorCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index 049879747..b11e45c3a 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -38,9 +38,14 @@ * Coordinates the generation of Fast Forward documentation frontpage and related reports. * This class MUST NOT be overridden and SHALL securely combine docs and testing commands. */ -#[AsCommand(name: 'reports', description: 'Generates the frontpage for Fast Forward documentation.')] +#[AsCommand( + name: 'standards:reports', + description: 'Generates the frontpage for Fast Forward documentation.', + aliases: ['reports'], +)] final class ReportsCommand extends Command -{ use HasCacheOption; +{ + use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php index a3a9f3762..726bec3e8 100644 --- a/src/Console/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -44,9 +44,14 @@ * target paths, triggers synchronization, and translates the resulting status * into Symfony Console output and process exit codes. */ -#[AsCommand(name: 'skills', description: 'Synchronizes Fast Forward skills into .agents/skills directory.')] +#[AsCommand( + name: 'agents:skills', + description: 'Synchronizes Fast Forward skills into .agents/skills directory.', + aliases: ['skills'] +)] final class SkillsCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; private const string SKILLS_DIRECTORY = '.agents/skills'; @@ -67,7 +72,7 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly LoggerInterface $logger, ) { - parent::__construct('skills'); + parent::__construct(); } /** diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index cbc520780..c2dd4c221 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -38,9 +38,14 @@ * Executes the full suite of Fast Forward code standard checks. * This class MUST NOT be modified through inheritance and SHALL streamline code validation workflows. */ -#[AsCommand(name: 'standards', description: 'Runs Fast Forward code standards checks.')] +#[AsCommand( + name: 'dev-tools:standards', + description: 'Runs Fast Forward code standards checks.', + aliases: ['standards'], +)] final class StandardsCommand extends Command -{ use HasCacheOption; +{ + use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index 5b67df890..82094c42f 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -36,12 +36,13 @@ * Orchestrates dev-tools synchronization commands for the consumer repository. */ #[AsCommand( - name: 'synchronize', + name: 'dev-tools:sync', description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, CODEOWNERS, .editorconfig, and .gitattributes in the root project.', - aliases: ['dev-tools:sync', 'sync'], + aliases: ['sync'], )] final class SyncCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 73c68a29d..9e050ee9d 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -46,9 +46,10 @@ * Facilitates the execution of the PHPUnit testing framework. * This class MUST NOT be overridden and SHALL configure testing parameters dynamically. */ -#[AsCommand(name: 'tests', description: 'Runs PHPUnit tests.')] +#[AsCommand(name: 'reports:tests', description: 'Runs PHPUnit tests.', aliases: ['phpunit', 'tests'])] final class TestsCommand extends Command -{ use HasCacheOption; +{ + use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index f20cfa68e..b8c95b7ac 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -45,11 +45,13 @@ * Updates composer.json with the Fast Forward dev-tools integration metadata. */ #[AsCommand( - name: 'update-composer-json', - description: 'Updates composer.json with Fast Forward dev-tools scripts and metadata.' + name: 'dev-tools:sync:composer', + description: 'Updates composer.json with Fast Forward dev-tools scripts and metadata.', + aliases: ['composer.json', 'update-composer-json'], )] final class UpdateComposerJsonCommand extends Command -{ use HasJsonOption; +{ + use HasJsonOption; use LogsCommandResults; /** diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index e5416c1cf..01d739402 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -43,9 +43,14 @@ * Handles the generation of API documentation for the project. * This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task. */ -#[AsCommand(name: 'wiki', description: 'Generates API documentation in Markdown format.')] +#[AsCommand( + name: 'github:wiki', + description: 'Generates API documentation in Markdown format.', + aliases: ['.github/wiki', 'wiki'], +)] final class WikiCommand extends Command -{ use HasCacheOption; +{ + use HasCacheOption; use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 50abd31f5..2f77321fc 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -19,6 +19,7 @@ namespace FastForward\DevTools\Console; +use Override; use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use DI\Container; use Psr\Container\ContainerInterface; @@ -31,6 +32,14 @@ */ final class DevTools extends Application { + private const string LOGO = <<<'LOGO' + ____ _____ _ + | _ \ _____ _|_ _|__ ___ | |___ + | | | |/ _ \ \ / / | |/ _ \ / _ \| / __| + | |_| | __/\ V / | | (_) | (_) | \__ \ + |____/ \___| \_/ |_|\___/ \___/|_|___/ + LOGO; + /** * @var ContainerInterface holds the static container instance for global access within the DevTools context */ @@ -52,6 +61,17 @@ public function __construct(CommandLoaderInterface $commandLoader) $this->setCommandLoader($commandLoader); } + /** + * Gets the help message for the DevTools application, including the ASCII logo. + * + * @return string + */ + #[Override] + public function getHelp(): string + { + return self::LOGO . "\n\n" . parent::getHelp(); + } + /** * Create DevTools instance from container. * diff --git a/src/Console/Output/OutputCapabilityDetector.php b/src/Console/Output/OutputCapabilityDetector.php index 95f0dffa4..0622edd3e 100644 --- a/src/Console/Output/OutputCapabilityDetector.php +++ b/src/Console/Output/OutputCapabilityDetector.php @@ -21,6 +21,9 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; +use Throwable; + +use function Safe\stream_isatty; /** * Detects ANSI-friendly output by decoration state or TTY-backed streams. @@ -44,6 +47,10 @@ public function supportsAnsi(OutputInterface $output): bool return false; } - return stream_isatty($output->getStream()); + try { + return stream_isatty($output->getStream()); + } catch (Throwable) { + return false; + } } } diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index b0ba09cdf..20276abc7 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -21,6 +21,7 @@ use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; use FastForward\DevTools\Composer\Command\ProxyCommand; +use FastForward\DevTools\Console\Command\FixtureWithoutAsCommand; use FastForward\DevTools\Console\DevTools; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -30,11 +31,10 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; use ReflectionProperty; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputDefinition; #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(DevTools::class)] +#[UsesClass(ProxyCommand::class)] final class DevToolsCommandProviderTest extends TestCase { use ProphecyTrait; @@ -84,24 +84,14 @@ public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): vo #[Test] public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCommands(): void { - $symfonyCommand = $this->prophesize(Command::class); - $inputDefinition = $this->prophesize(InputDefinition::class); - - $symfonyCommand->getName() - ->willReturn('agents'); - $symfonyCommand->getAliases() - ->willReturn([]); - $symfonyCommand->getDescription() - ->willReturn('Synchronize agents.'); - $symfonyCommand->getHelp() - ->willReturn(''); - $symfonyCommand->getDefinition() - ->willReturn($inputDefinition->reveal()); - $symfonyCommand->isHidden() - ->willReturn(false); + $symfonyCommand = new FixtureWithoutAsCommand('agents'); + $symfonyCommand->setAliases([]); + $symfonyCommand->setDescription('Synchronize agents.'); + $symfonyCommand->setHelp(''); + $symfonyCommand->setHidden(false); $this->devTools->all() - ->willReturn([$symfonyCommand->reveal()]) + ->willReturn([$symfonyCommand]) ->shouldBeCalledOnce(); $commands = $this->commandProvider->getCommands(); diff --git a/tests/Composer/Json/ComposerJsonTest.php b/tests/Composer/Json/ComposerJsonTest.php index 7719a7517..00335569a 100644 --- a/tests/Composer/Json/ComposerJsonTest.php +++ b/tests/Composer/Json/ComposerJsonTest.php @@ -26,6 +26,7 @@ use FastForward\DevTools\Composer\Json\Schema\Funding; use FastForward\DevTools\Composer\Json\Schema\Support; use FastForward\DevTools\Composer\Json\Schema\SupportInterface; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -41,6 +42,7 @@ #[UsesClass(Author::class)] #[UsesClass(Funding::class)] #[UsesClass(Support::class)] +#[UsesClass(WorkingProjectPathResolver::class)] final class ComposerJsonTest extends TestCase { /** diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index 673b3af2e..d2e1b7b08 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -132,7 +132,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('codeowners', $this->command->getName()); + self::assertSame('github:codeowners', $this->command->getName()); self::assertSame( 'Generates .github/CODEOWNERS from local project metadata.', $this->command->getDescription(), diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index 14cfdc958..df7a3fda1 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -132,7 +132,7 @@ protected function tearDown(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('copy-resource', $this->command->getName()); + self::assertSame('dev-tools:sync:copy', $this->command->getName()); self::assertSame( 'Copies a file or directory resource into the current project.', $this->command->getDescription() diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index 32335487e..47e658440 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -615,7 +615,7 @@ public function executeWillPassWorkingDirectoryAndAlternateManifestToComposerNor #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('funding', $this->command->getName()); + self::assertSame('github:funding', $this->command->getName()); self::assertSame( 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.', $this->command->getDescription(), diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index c41dfc304..f25f85a21 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -179,7 +179,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('gitattributes', $this->command->getName()); + self::assertSame('git:attributes', $this->command->getName()); self::assertSame( 'Manages .gitattributes export-ignore rules for leaner package archives.', $this->command->getDescription() diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index 84b37d609..beb38ce59 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -136,7 +136,7 @@ protected function tearDown(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('git-hooks', $this->command->getName()); + self::assertSame('git:hooks', $this->command->getName()); self::assertSame('Installs Fast Forward Git hooks.', $this->command->getDescription()); self::assertSame( 'This command copies packaged Git hooks into the current repository.', diff --git a/tests/Console/Command/GitIgnoreCommandTest.php b/tests/Console/Command/GitIgnoreCommandTest.php index 318fd2851..4bebec70a 100644 --- a/tests/Console/Command/GitIgnoreCommandTest.php +++ b/tests/Console/Command/GitIgnoreCommandTest.php @@ -192,7 +192,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('gitignore', $this->command->getName()); + self::assertSame('git:ignore', $this->command->getName()); self::assertSame('Merges and synchronizes .gitignore files.', $this->command->getDescription()); self::assertSame( "This command merges the canonical .gitignore from dev-tools with the project's existing .gitignore.", diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index 5596a79e2..598c680bc 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -132,7 +132,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('license', $this->command->getName()); + self::assertSame('license:generate', $this->command->getName()); self::assertSame( 'Generates a LICENSE file from composer.json license information.', $this->command->getDescription() diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 9d1576b59..6f09cbedf 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -25,6 +25,7 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -42,6 +43,7 @@ #[CoversClass(ReportsCommand::class)] #[UsesClass(ManagedWorkspace::class)] +#[UsesClass(DevToolsPathResolver::class)] #[UsesTrait(LogsCommandResults::class)] final class ReportsCommandTest extends TestCase { diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index e7400b197..9ebab4d62 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -35,11 +35,13 @@ use Psr\Log\LoggerInterface; use ReflectionMethod; use FastForward\DevTools\Path\ManagedWorkspace; +use FastForward\DevTools\Path\DevToolsPathResolver; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[CoversClass(StandardsCommand::class)] #[UsesClass(ManagedWorkspace::class)] +#[UsesClass(DevToolsPathResolver::class)] #[UsesTrait(LogsCommandResults::class)] final class StandardsCommandTest extends TestCase { diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index 19d5d0e25..5427826ec 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -113,7 +113,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('update-composer-json', $this->command->getName()); + self::assertSame('dev-tools:sync:composer', $this->command->getName()); self::assertSame( 'Updates composer.json with Fast Forward dev-tools scripts and metadata.', $this->command->getDescription() diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index 720efcf5f..2175348da 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -133,7 +133,6 @@ public function constructorWillRegisterCommandAliasesFromAsCommandAttribute(): v $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); - self::assertTrue($loader->has('synchronize')); self::assertTrue($loader->has('dev-tools:sync')); self::assertTrue($loader->has('sync')); } diff --git a/tests/Fixtures/composer-plugin-consumer/.gitignore b/tests/Fixtures/composer-plugin-consumer/.gitignore index ed3675727..3e76ade8a 100644 --- a/tests/Fixtures/composer-plugin-consumer/.gitignore +++ b/tests/Fixtures/composer-plugin-consumer/.gitignore @@ -1,3 +1,12 @@ -* +.dev-tools/ +.idea/ +.vscode/ +backup/ +tmp/ +vendor/ !.gitignore !composer.json +* +*.cache +.DS_Store +composer.lock diff --git a/tests/Fixtures/composer-plugin-consumer/composer.json b/tests/Fixtures/composer-plugin-consumer/composer.json index 765085a09..1919878a9 100644 --- a/tests/Fixtures/composer-plugin-consumer/composer.json +++ b/tests/Fixtures/composer-plugin-consumer/composer.json @@ -3,7 +3,7 @@ "description": "Fixture project used to verify DevTools Composer plugin consumer behavior.", "license": "MIT", "type": "project", - "require-dev": { + "require": { "fast-forward/dev-tools": "*@dev" }, "repositories": [ From 9b226a9fd09215d278057c4aae9fbf7d533c4327 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:30:36 +0000 Subject: [PATCH 10/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index bbb6338bd..991f49a38 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit bbb6338bd0c1bb51571a3c102ae5ce21421e5138 +Subproject commit 991f49a387e695bffede52209677ba192e73e71f From d4b344a0268ffbd38398024fe6436c8cc656d73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 05:03:17 -0300 Subject: [PATCH 11/31] fix: align composer command provider test with proxy map keys --- .../Capability/DevToolsCommandProvider.php | 26 ++++++++++++------- .../DevToolsCommandProviderTest.php | 5 ++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index c23468ffd..7d8bfe49f 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -19,11 +19,9 @@ namespace FastForward\DevTools\Composer\Capability; -use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; -use Symfony\Component\Console\Command\Command; /** * Provides a registry of custom dev-tools commands mapped for Composer integration. @@ -38,12 +36,22 @@ final class DevToolsCommandProvider implements CommandProvider */ public function getCommands() { - return array_map( - static fn(Command $command): BaseCommand => new ProxyCommand($command), - array_filter( - DevTools::create()->all(), - static fn(Command $command): bool => str_starts_with($command::class, self::COMMAND_NAMESPACE), - ), - ); + $commands = []; + + foreach (DevTools::create()->all() as $command) { + if (! str_starts_with($command::class, self::COMMAND_NAMESPACE)) { + continue; + } + + $id = spl_object_hash($command); + + if (isset($commands[$id])) { + continue; + } + + $commands[$id] = new ProxyCommand($command); + } + + return $commands; } } diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 20276abc7..589deddd5 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -95,10 +95,11 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo ->shouldBeCalledOnce(); $commands = $this->commandProvider->getCommands(); + $command = array_values($commands)[0]; self::assertIsArray($commands); self::assertCount(1, $commands); - self::assertInstanceOf(ProxyCommand::class, $commands[0]); - self::assertSame('agents', $commands[0]->getName()); + self::assertInstanceOf(ProxyCommand::class, $command); + self::assertSame('agents', $command->getName()); } } From d79d461b201fdb9bb4f3d2a6b17336475fbf6e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 15:25:13 -0300 Subject: [PATCH 12/31] refactor: centralize command retrieval through DevTools API --- .../Capability/DevToolsCommandProvider.php | 24 +++----------- src/Console/DevTools.php | 19 +++++++++-- .../DevToolsCommandProviderTest.php | 4 +-- tests/Console/DevToolsTest.php | 33 +++++++++++++++++++ 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index 7d8bfe49f..0a33fb239 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -22,6 +22,7 @@ use Composer\Plugin\Capability\CommandProvider; use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; +use Symfony\Component\Console\Command\Command; /** * Provides a registry of custom dev-tools commands mapped for Composer integration. @@ -29,29 +30,14 @@ */ final class DevToolsCommandProvider implements CommandProvider { - private const string COMMAND_NAMESPACE = 'FastForward\\DevTools\\Console\\Command\\'; - /** * {@inheritDoc} */ public function getCommands() { - $commands = []; - - foreach (DevTools::create()->all() as $command) { - if (! str_starts_with($command::class, self::COMMAND_NAMESPACE)) { - continue; - } - - $id = spl_object_hash($command); - - if (isset($commands[$id])) { - continue; - } - - $commands[$id] = new ProxyCommand($command); - } - - return $commands; + return array_map( + static fn(Command $command): ProxyCommand => new ProxyCommand($command), + iterator_to_array(DevTools::create()->getCommands()), + ); } } diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 2f77321fc..53f0ad12a 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -53,12 +53,13 @@ final class DevTools extends Application * * @param CommandLoaderInterface $commandLoader the command loader responsible for providing command instances */ - public function __construct(CommandLoaderInterface $commandLoader) - { + public function __construct( + private readonly CommandLoaderInterface $commandLoader + ) { parent::__construct('Fast Forward Dev Tools'); $this->setDefaultCommand('standards'); - $this->setCommandLoader($commandLoader); + $this->setCommandLoader($this->commandLoader); } /** @@ -72,6 +73,18 @@ public function getHelp(): string return self::LOGO . "\n\n" . parent::getHelp(); } + /** + * Retrieves the shared DevTools service container. + * + * @return iterable + */ + public function getCommands(): iterable + { + foreach ($this->commandLoader->getNames() as $name) { + yield $name => $this->commandLoader->get($name); + } + } + /** * Create DevTools instance from container. * diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 589deddd5..ea1fe2298 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -57,7 +57,7 @@ protected function setUp(): void ->willReturn($this->devTools->reveal()) ->shouldBeCalledOnce(); - $this->devTools->all() + $this->devTools->getCommands() ->willReturn([])->shouldBeCalledOnce(); $this->commandProvider = new DevToolsCommandProvider(); @@ -90,7 +90,7 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->all() + $this->devTools->getCommands() ->willReturn([$symfonyCommand]) ->shouldBeCalledOnce(); diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index b9995c9ae..17cbe805b 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -110,6 +110,39 @@ public function __construct() self::assertSame($customCommand, $this->devTools->get('custom')); } + /** + * @return void + */ + #[Test] + public function getCommandsWillYieldLoaderCommandsWithPreservedKeys(): void + { + $commands = [ + 'agents' => new class extends Command { + public function __construct() + { + parent::__construct('agents'); + } + }, + 'sync' => new class extends Command { + public function __construct() + { + parent::__construct('sync'); + } + }, + ]; + + $this->commandLoader->getNames() + ->willReturn(array_keys($commands)); + $this->commandLoader->get('agents') + ->willReturn($commands['agents']); + $this->commandLoader->get('sync') + ->willReturn($commands['sync']); + + $providedCommands = iterator_to_array($this->devTools->getCommands()); + + self::assertSame($commands, $providedCommands); + } + /** * @return void */ From 139ec4d9cda175c96f952bb544a867a3c312683b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 18:26:37 +0000 Subject: [PATCH 13/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 991f49a38..724bc777a 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 991f49a387e695bffede52209677ba192e73e71f +Subproject commit 724bc777aaea2f54d7dde7277c8bf1707d098506 From bda11dcf09158a621223fd1cd611058b4a6e1cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 17:31:32 -0300 Subject: [PATCH 14/31] fix: avoid registering command aliases in composer provider --- .../Capability/DevToolsCommandProvider.php | 25 +++++++++++++++---- src/Console/DevTools.php | 21 +++------------- .../DevToolsCommandProviderTest.php | 8 +++--- .../DevToolsCommandLoaderTest.php | 3 +-- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index 0a33fb239..c0a9b7743 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -22,7 +22,6 @@ use Composer\Plugin\Capability\CommandProvider; use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; -use Symfony\Component\Console\Command\Command; /** * Provides a registry of custom dev-tools commands mapped for Composer integration. @@ -30,14 +29,30 @@ */ final class DevToolsCommandProvider implements CommandProvider { + /** + * @var string the namespace prefix for dev-tools console commands to be registered as Composer commands + */ + private const string COMMAND_NAMESPACE = 'FastForward\DevTools\Console\Command'; + /** * {@inheritDoc} */ public function getCommands() { - return array_map( - static fn(Command $command): ProxyCommand => new ProxyCommand($command), - iterator_to_array(DevTools::create()->getCommands()), - ); + $commands = []; + + foreach (DevTools::create()->all() as $registeredName => $command) { + if ($registeredName !== $command->getName()) { + continue; + } + + if (! str_starts_with($command::class, self::COMMAND_NAMESPACE)) { + continue; + } + + $commands[] = new ProxyCommand($command); + } + + return $commands; } } diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 53f0ad12a..1e2c20668 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -53,13 +53,12 @@ final class DevTools extends Application * * @param CommandLoaderInterface $commandLoader the command loader responsible for providing command instances */ - public function __construct( - private readonly CommandLoaderInterface $commandLoader - ) { + public function __construct(CommandLoaderInterface $commandLoader) + { parent::__construct('Fast Forward Dev Tools'); - $this->setDefaultCommand('standards'); - $this->setCommandLoader($this->commandLoader); + $this->setDefaultCommand('dev-tools:standards'); + $this->setCommandLoader($commandLoader); } /** @@ -73,18 +72,6 @@ public function getHelp(): string return self::LOGO . "\n\n" . parent::getHelp(); } - /** - * Retrieves the shared DevTools service container. - * - * @return iterable - */ - public function getCommands(): iterable - { - foreach ($this->commandLoader->getNames() as $name) { - yield $name => $this->commandLoader->get($name); - } - } - /** * Create DevTools instance from container. * diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index ea1fe2298..691fb341c 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -57,7 +57,7 @@ protected function setUp(): void ->willReturn($this->devTools->reveal()) ->shouldBeCalledOnce(); - $this->devTools->getCommands() + $this->devTools->all() ->willReturn([])->shouldBeCalledOnce(); $this->commandProvider = new DevToolsCommandProvider(); @@ -90,8 +90,10 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->getCommands() - ->willReturn([$symfonyCommand]) + $this->devTools->all() + ->willReturn([ + 'agents' => $symfonyCommand, + ]) ->shouldBeCalledOnce(); $commands = $this->commandProvider->getCommands(); diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index 2175348da..f32f76520 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -108,7 +108,7 @@ public function constructorWillRegisterOnlyInstantiableCommands(): void * @return void */ #[Test] - public function constructorWillRegisterCommandAliasesFromAsCommandAttribute(): void + public function constructorWillRegisterPrimaryCommandFromAsCommandAttribute(): void { $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; @@ -134,7 +134,6 @@ public function constructorWillRegisterCommandAliasesFromAsCommandAttribute(): v $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); self::assertTrue($loader->has('dev-tools:sync')); - self::assertTrue($loader->has('sync')); } /** From 3b703e4308e96eafa0fc76e0585f1fb2ec984a6f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:32:41 +0000 Subject: [PATCH 15/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 724bc777a..219a9117b 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 724bc777aaea2f54d7dde7277c8bf1707d098506 +Subproject commit 219a9117ba3404bce788febb2d3d60cb03b40155 From 55f75eec34178bba84e8f3af7cd5b7ba21028378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 17:34:00 -0300 Subject: [PATCH 16/31] test: guard composer provider against alias-only command entries --- .../DevToolsCommandProviderTest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 691fb341c..573fdad23 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -104,4 +104,30 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo self::assertInstanceOf(ProxyCommand::class, $command); self::assertSame('agents', $command->getName()); } + + /** + * @return void + */ + #[Test] + public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): void + { + $symfonyCommand = new FixtureWithoutAsCommand('reports:tests'); + $symfonyCommand->setAliases(['tests']); + $symfonyCommand->setDescription('Runs PHPUnit tests.'); + $symfonyCommand->setHelp(''); + $symfonyCommand->setHidden(false); + + $this->devTools->all() + ->willReturn([ + 'reports:tests' => $symfonyCommand, + 'tests' => $symfonyCommand, + ]) + ->shouldBeCalledOnce(); + + $commands = $this->commandProvider->getCommands(); + + self::assertCount(1, $commands); + self::assertInstanceOf(ProxyCommand::class, $commands[0]); + self::assertSame('reports:tests', $commands[0]->getName()); + } } From 68b2f51e8d3dddf7daffd070c6be551cf4390f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 17:35:10 -0300 Subject: [PATCH 17/31] test: align DevTools test with Application::all loader contract --- tests/Console/DevToolsTest.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index 17cbe805b..75f58f133 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -114,7 +114,7 @@ public function __construct() * @return void */ #[Test] - public function getCommandsWillYieldLoaderCommandsWithPreservedKeys(): void + public function allWillReturnLoaderCommandsWithPreservedKeys(): void { $commands = [ 'agents' => new class extends Command { @@ -133,14 +133,23 @@ public function __construct() $this->commandLoader->getNames() ->willReturn(array_keys($commands)); + $this->commandLoader->has('agents') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->has('sync') + ->willReturn(true) + ->shouldBeCalledOnce(); $this->commandLoader->get('agents') ->willReturn($commands['agents']); $this->commandLoader->get('sync') ->willReturn($commands['sync']); - $providedCommands = iterator_to_array($this->devTools->getCommands()); + $providedCommands = $this->devTools->all(); - self::assertSame($commands, $providedCommands); + self::assertArrayHasKey('agents', $providedCommands); + self::assertArrayHasKey('sync', $providedCommands); + self::assertSame($commands['agents'], $providedCommands['agents']); + self::assertSame($commands['sync'], $providedCommands['sync']); } /** From aed6356df9ce1b62e89d4f2e6c8e19c7e7f1b126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 17:43:15 -0300 Subject: [PATCH 18/31] test: document alias filtering and cover command alias registration --- .../Capability/DevToolsCommandProvider.php | 5 +++ .../DevToolsCommandLoaderTest.php | 35 +++++++++++++++++++ tests/Console/DevToolsTest.php | 3 ++ 3 files changed, 43 insertions(+) diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index c0a9b7743..76501fdfb 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -42,6 +42,11 @@ public function getCommands() $commands = []; foreach (DevTools::create()->all() as $registeredName => $command) { + /** + * Composer plugin registrations must be canonicalized to one command per Symfony command. + * The application exposes alias keys in `all()`, but Composer interprets each entry as + * an independent command and emits override warnings. + */ if ($registeredName !== $command->getName()) { continue; } diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index f32f76520..d742b2290 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -22,6 +22,7 @@ use ArrayIterator; use FastForward\DevTools\Console\Command\AgentsCommand; use FastForward\DevTools\Console\Command\SyncCommand; +use FastForward\DevTools\Console\Command\TestsCommand; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use PHPUnit\Framework\Attributes\CoversClass; @@ -136,6 +137,40 @@ public function constructorWillRegisterPrimaryCommandFromAsCommandAttribute(): v self::assertTrue($loader->has('dev-tools:sync')); } + /** + * @return void + */ + #[Test] + public function constructorWillRegisterAliasesFromAsCommandAttribute(): void + { + $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; + + $this->finderFactory->create() + ->willReturn($this->finder->reveal()) + ->shouldBeCalledOnce(); + $this->finder->files() + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled(); + $this->finder->notPath('Traits') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->name('*.php') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo($commandDirectory . '/TestsCommand.php', '', 'TestsCommand.php'), + ]))->shouldBeCalled(); + $this->container->has(TestsCommand::class)->willReturn(true)->shouldBeCalled(); + + $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); + + self::assertTrue($loader->has('reports:tests')); + self::assertTrue($loader->has('tests')); + self::assertTrue($loader->has('phpunit')); + } + /** * @return void */ diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index 75f58f133..15406e239 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -31,6 +31,7 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use ReflectionMethod; use ReflectionProperty; @@ -67,6 +68,8 @@ protected function setUp(): void $this->commandLoader = $this->prophesize(CommandLoaderInterface::class); $this->commandLoader->getNames() ->willReturn([]); + $this->commandLoader->has(Argument::type('string')) + ->willReturn(false); $this->devTools = new DevTools($this->commandLoader->reveal()); } From dd8834eb113be4b5097e61e4507b53159afd8a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 17:46:33 -0300 Subject: [PATCH 19/31] test: cover alias preservation in composer command provider --- .../DevToolsCommandProviderTest.php | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 573fdad23..40ca1f45a 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -112,7 +112,7 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): void { $symfonyCommand = new FixtureWithoutAsCommand('reports:tests'); - $symfonyCommand->setAliases(['tests']); + $symfonyCommand->setAliases(['tests', 'phpunit']); $symfonyCommand->setDescription('Runs PHPUnit tests.'); $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); @@ -129,5 +129,33 @@ public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): v self::assertCount(1, $commands); self::assertInstanceOf(ProxyCommand::class, $commands[0]); self::assertSame('reports:tests', $commands[0]->getName()); + self::assertSame(['tests', 'phpunit'], $commands[0]->getAliases()); + } + + /** + * @return void + */ + #[Test] + public function getCommandsWillPreserveAliasDefinitionsInProxyCommand(): void + { + $symfonyCommand = new FixtureWithoutAsCommand('dev-tools:standards'); + $symfonyCommand->setAliases(['standards']); + $symfonyCommand->setDescription('Runs standards checks.'); + $symfonyCommand->setHelp(''); + $symfonyCommand->setHidden(false); + + $this->devTools->all() + ->willReturn([ + 'dev-tools:standards' => $symfonyCommand, + 'standards' => $symfonyCommand, + ]) + ->shouldBeCalledOnce(); + + $commands = $this->commandProvider->getCommands(); + $proxyCommand = $commands[0]; + + self::assertInstanceOf(ProxyCommand::class, $proxyCommand); + self::assertSame('dev-tools:standards', $proxyCommand->getName()); + self::assertSame(['standards'], $proxyCommand->getAliases()); } } From 56a0b61b902242c5224a5bf2de46c4c68608c712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 17:48:18 -0300 Subject: [PATCH 20/31] test: avoid command provider assertions depending on map keys --- .../Capability/DevToolsCommandProviderTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 40ca1f45a..6f5a5a9c7 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -96,8 +96,8 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo ]) ->shouldBeCalledOnce(); - $commands = $this->commandProvider->getCommands(); - $command = array_values($commands)[0]; + $commands = array_values($this->commandProvider->getCommands()); + $command = $commands[0]; self::assertIsArray($commands); self::assertCount(1, $commands); @@ -124,12 +124,13 @@ public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): v ]) ->shouldBeCalledOnce(); - $commands = $this->commandProvider->getCommands(); + $commands = array_values($this->commandProvider->getCommands()); + $proxyCommand = $commands[0]; self::assertCount(1, $commands); - self::assertInstanceOf(ProxyCommand::class, $commands[0]); - self::assertSame('reports:tests', $commands[0]->getName()); - self::assertSame(['tests', 'phpunit'], $commands[0]->getAliases()); + self::assertInstanceOf(ProxyCommand::class, $proxyCommand); + self::assertSame('reports:tests', $proxyCommand->getName()); + self::assertSame(['tests', 'phpunit'], $proxyCommand->getAliases()); } /** @@ -151,8 +152,7 @@ public function getCommandsWillPreserveAliasDefinitionsInProxyCommand(): void ]) ->shouldBeCalledOnce(); - $commands = $this->commandProvider->getCommands(); - $proxyCommand = $commands[0]; + $proxyCommand = array_values($this->commandProvider->getCommands())[0]; self::assertInstanceOf(ProxyCommand::class, $proxyCommand); self::assertSame('dev-tools:standards', $proxyCommand->getName()); From c87c3375ab45222a1632bd333387d8f61be82802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 18:11:11 -0300 Subject: [PATCH 21/31] chore: align standards php-cs-fixer naming and harden command loader --- .github/wiki | 2 +- CHANGELOG.md | 5 +++ src/Console/Command/PhpDocCommand.php | 5 ++- src/Console/Command/StandardsCommand.php | 8 ++-- .../CommandLoader/DevToolsCommandLoader.php | 17 +++++++- .../Console/Command/StandardsCommandTest.php | 6 +-- .../DevToolsCommandLoaderTest.php | 42 +++++++++++++++++++ .../Command/FixtureDuplicateCommandName.php | 26 ++++++++++++ 8 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php diff --git a/.github/wiki b/.github/wiki index 219a9117b..bbb6338bd 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 219a9117ba3404bce788febb2d3d60cb03b40155 +Subproject commit bbb6338bd0c1bb51571a3c102ae5ce21421e5138 diff --git a/CHANGELOG.md b/CHANGELOG.md index daf4c466e..7a029d7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add a hybrid command runtime bootstrap and capability bridge that keeps command discovery split between migrated Symfony commands (`DevTools`) and legacy Composer `BaseCommand` commands (`DevToolsComposer`) while exposing proxy commands during Composer execution for the first migration step (#199) +### Changed + +- Align the standards pipeline to invoke PHPDoc checks via the `php-cs-fixer` command name/alias and route standards cache to `.dev-tools/cache/php-cs-fixer`, preserving backwards compatibility for existing `dockblock`-style entry points. +- Rename the command entry point from `standards:phpdoc` to `standards:dockblock` and keep both `dockblock` and `php-cs-fixer` aliases for compatibility (`standards:phpdoc` is intentionally not kept as a command name). + ## [1.22.3] - 2026-04-25 ### Fixed diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 0482cdb8c..7b2ab9232 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -44,7 +44,10 @@ * Provides operations to inspect, lint, and repair PHPDoc comments across the project. * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ -#[AsCommand(name: 'standards:phpdoc', description: 'Checks and fixes PHPDocs.', aliases: ['phpdoc'])] +#[AsCommand(name: 'standards:dockblock', description: 'Checks and fixes PHPDocs.', aliases: [ + 'dockblock', + 'php-cs-fixer', +])] final class PhpDocCommand extends Command { use HasCacheOption; diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index c2dd4c221..0b141afa3 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -113,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'input' => $input, ]); - foreach (['refactor', 'phpdoc', 'code-style', 'reports'] as $command) { + foreach (['refactor', 'php-cs-fixer', 'code-style', 'reports'] as $command) { $commands[] = $command; $processBuilder = $this->processBuilder; @@ -129,13 +129,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $processBuilder = $processBuilder->withArgument('--fix'); } - if (\in_array($command, ['phpdoc', 'reports'], true) && null !== $cacheArgument) { + if (\in_array($command, ['php-cs-fixer', 'reports'], true) && null !== $cacheArgument) { $processBuilder = $processBuilder->withArgument($cacheArgument); } if ( $cacheDirEnabled - && \in_array($command, ['phpdoc', 'reports'], true) + && \in_array($command, ['php-cs-fixer', 'reports'], true) && null !== $cacheDir = $this->resolveCacheDirArgument($input, $command) ) { $processBuilder = $processBuilder->withArgument('--cache-dir', $cacheDir); @@ -181,7 +181,7 @@ private function getProcessLabel(string $command): string { return match ($command) { 'refactor' => 'Refactoring Code with DevTools', - 'phpdoc' => 'Checking PHPDoc with DevTools', + 'php-cs-fixer', 'dockblock' => 'Checking PHPDoc with DevTools', 'code-style' => 'Checking Code Style with DevTools', 'reports' => 'Generating Reports with DevTools', default => 'Running DevTools Command', diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index 11713711e..22d033220 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Filesystem\FinderFactoryInterface; use Psr\Container\ContainerInterface; use ReflectionClass; +use RuntimeException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; @@ -96,13 +97,27 @@ private function getCommandMap(FinderFactoryInterface $finderFactory): array } $arguments = $attribute->getArguments(); - $commandNames = [$arguments['name'], ...($arguments['aliases'] ?? [])]; + $commandName = $arguments['name'] ?? $arguments[0] ?? ''; + $aliases = $arguments['aliases'] ?? $arguments[2] ?? []; + $commandNames = [$commandName, ...((array) $aliases)]; foreach ($commandNames as $commandName) { + if (! \is_string($commandName)) { + continue; + } + if ('' === $commandName) { continue; } + if (\array_key_exists($commandName, $commandMap) && $commandMap[$commandName] !== $class) { + throw new RuntimeException(\sprintf( + 'Command %s is already registered and cannot be assigned to %s.', + $commandName, + $class + )); + } + $commandMap[$commandName] = $class; } } diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index 9ebab4d62..764fa6bc4 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -118,7 +118,7 @@ public function executeWillRunSuiteSequentially(): void 'Code standards checks completed successfully.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface && $context['output'] instanceof OutputInterface - && ['refactor', 'phpdoc', 'code-style', 'reports'] === $context['commands']), + && ['refactor', 'php-cs-fixer', 'code-style', 'reports'] === $context['commands']), )->shouldBeCalled(); self::assertSame(StandardsCommand::SUCCESS, $this->invokeExecute()); @@ -143,7 +143,7 @@ public function executeWillReturnFailureWhenAnyCommandFails(): void 'Code standards checks failed.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface && $context['output'] instanceof OutputInterface - && ['refactor', 'phpdoc', 'code-style', 'reports'] === $context['commands']), + && ['refactor', 'php-cs-fixer', 'code-style', 'reports'] === $context['commands']), )->shouldBeCalled(); self::assertSame(StandardsCommand::FAILURE, $this->invokeExecute()); @@ -162,7 +162,7 @@ public function executeWithCacheWillForwardCacheOnlyToCacheAwareNestedCommands() $this->processBuilder->withArgument('--cache') ->willReturn($this->processBuilder->reveal()) ->shouldBeCalledTimes(2); - $this->processBuilder->withArgument('--cache-dir', '.dev-tools/cache/phpdoc') + $this->processBuilder->withArgument('--cache-dir', '.dev-tools/cache/php-cs-fixer') ->willReturn($this->processBuilder->reveal()) ->shouldBeCalledOnce(); $this->processBuilder->withArgument('--cache-dir', '.dev-tools/cache/reports') diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index d742b2290..9d8d26f59 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -25,6 +25,7 @@ use FastForward\DevTools\Console\Command\TestsCommand; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use FastForward\DevTools\Filesystem\FinderFactoryInterface; +use RuntimeException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -171,6 +172,47 @@ public function constructorWillRegisterAliasesFromAsCommandAttribute(): void self::assertTrue($loader->has('phpunit')); } + /** + * @return void + */ + #[Test] + public function constructorWillFailWhenCommandNameConflicts(): void + { + $commandDirectory = \dirname(__DIR__, 3); + $fixtureDirectory = $commandDirectory . '/tests/Fixtures/Console/Command'; + $srcDirectory = $commandDirectory . '/src/Console/Command'; + + $this->finderFactory->create() + ->willReturn($this->finder->reveal()) + ->shouldBeCalledOnce(); + $this->finder->files() + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled(); + $this->finder->notPath('Traits') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->name('*.php') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo( + $fixtureDirectory . '/FixtureDuplicateCommandName.php', + '', + 'FixtureDuplicateCommandName.php' + ), + new SplFileInfo($srcDirectory . '/AgentsCommand.php', '', 'AgentsCommand.php'), + ]))->shouldBeCalled(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'Command agents is already registered and cannot be assigned to FastForward\\DevTools\\Console\\Command\\AgentsCommand.' + ); + + new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); + } + /** * @return void */ diff --git a/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php b/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php new file mode 100644 index 000000000..ae023fd3f --- /dev/null +++ b/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php @@ -0,0 +1,26 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Console\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; + +#[AsCommand(name: 'agents')] +final class FixtureDuplicateCommandName extends Command {} From efef8cc19afb2f45886218ab4d6fab7c9684c245 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:12:03 +0000 Subject: [PATCH 22/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index bbb6338bd..219a9117b 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit bbb6338bd0c1bb51571a3c102ae5ce21421e5138 +Subproject commit 219a9117ba3404bce788febb2d3d60cb03b40155 From 94b74e5757877cc7864fe2a0ccd596597a53facb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 18:22:36 -0300 Subject: [PATCH 23/31] refactor: rename dockblock references to docheader --- CHANGELOG.md | 4 ++-- README.md | 2 +- docs/advanced/rector-and-phpdoc.rst | 12 +++++------ docs/api/commands.rst | 5 +++-- docs/commands/phpdoc.rst | 24 ++++++++++++---------- docs/commands/standards.rst | 6 +++--- docs/configuration/overriding-defaults.rst | 4 ++-- docs/configuration/tooling-defaults.rst | 6 +++--- docs/running/specialized-commands.rst | 8 ++++---- docs/running/unified-command.rst | 4 ++-- docs/troubleshooting.rst | 2 +- docs/usage/common-workflows.rst | 2 +- src/Console/Command/PhpDocCommand.php | 4 ++-- src/Console/Command/StandardsCommand.php | 2 +- 14 files changed, 44 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a029d7c6..ae5c5555a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Align the standards pipeline to invoke PHPDoc checks via the `php-cs-fixer` command name/alias and route standards cache to `.dev-tools/cache/php-cs-fixer`, preserving backwards compatibility for existing `dockblock`-style entry points. -- Rename the command entry point from `standards:phpdoc` to `standards:dockblock` and keep both `dockblock` and `php-cs-fixer` aliases for compatibility (`standards:phpdoc` is intentionally not kept as a command name). +- Align the standards pipeline to invoke PHPDoc checks via the `php-cs-fixer` command name/alias and route standards cache to `.dev-tools/cache/php-cs-fixer`, preserving backwards compatibility for existing `docheader`-style entry points. +- Rename the command entry point from `standards:phpdoc` to `standards:docheader` and keep both `docheader` and `php-cs-fixer` aliases for compatibility (`standards:phpdoc` is intentionally not kept as a command name). ## [1.22.3] - 2026-04-25 diff --git a/README.md b/README.md index fb441f6cd..f95321b6f 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ composer code-style composer refactor # Check and fix PHPDoc comments -composer phpdoc +composer docheader # Generate HTML API documentation using phpDocumentor composer docs diff --git a/docs/advanced/rector-and-phpdoc.rst b/docs/advanced/rector-and-phpdoc.rst index 27e80d883..c357dae4a 100644 --- a/docs/advanced/rector-and-phpdoc.rst +++ b/docs/advanced/rector-and-phpdoc.rst @@ -1,14 +1,14 @@ -Rector and PHPDoc Automation -============================ +Rector and Docheader Automation +=============================== The package uses two different Rector entry points, and that difference matters when you are trying to understand why a rule did or did not run. -``refactor`` Versus ``phpdoc`` +``refactor`` Versus ``docheader`` ------------------------------ - ``refactor`` uses the full ``rector.php`` file. -- ``phpdoc`` runs PHP-CS-Fixer first and then executes Rector with +- ``docheader`` runs PHP-CS-Fixer first and then executes Rector with ``--only \FastForward\DevTools\Rector\AddMissingMethodPhpDocRector``. Rules Shipped by the Package @@ -19,7 +19,7 @@ Rules Shipped by the Package * - Rule - Enabled in packaged ``rector.php`` - - Used directly by ``phpdoc`` + - Used directly by ``docheader`` - Purpose * - ``FastForward\DevTools\Rector\AddMissingMethodPhpDocRector`` - Yes @@ -45,7 +45,7 @@ rules when ``thecodingmachine/safe`` is installed. Why ``.docheader`` Appears Automatically ---------------------------------------- -The ``phpdoc`` command creates ``.docheader`` in the consumer root when it is +The ``docheader`` command creates ``.docheader`` in the consumer root when it is missing. The template comes from the packaged file and the package name is rewritten to match the current project whenever Composer metadata is available. diff --git a/docs/api/commands.rst b/docs/api/commands.rst index e31e851bc..8b271405e 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -37,8 +37,9 @@ subprocess execution is needed. - ``refactor`` - Runs Rector with local or packaged configuration. * - ``FastForward\DevTools\Console\Command\PhpDocCommand`` - - ``phpdoc`` - - Runs PHP-CS-Fixer and a focused Rector PHPDoc pass. + - ``standards:docheader`` + - Runs PHP-CS-Fixer and a focused Rector PHPDoc pass. Supported aliases: + ``docheader`` and ``php-cs-fixer``. * - ``FastForward\DevTools\Console\Command\CodeStyleCommand`` - ``code-style`` - Runs Composer Normalize and ECS. diff --git a/docs/commands/phpdoc.rst b/docs/commands/phpdoc.rst index 58e5d95ef..9ff83e4e0 100644 --- a/docs/commands/phpdoc.rst +++ b/docs/commands/phpdoc.rst @@ -1,12 +1,13 @@ -phpdoc -====== +docheader +========= Checks and fixes PHPDoc comments. Description ----------- -The ``phpdoc`` command coordinates PHPDoc checking and fixing using: +The ``docheader`` command coordinates PHPDoc checking and fixing using: +(alias ``docheader`` and ``php-cs-fixer``) - PHP-CS-Fixer - fixes PHPDoc formatting - Rector with ``AddMissingMethodPhpDocRector`` - adds missing method PHPDoc @@ -18,10 +19,11 @@ Usage .. code-block:: bash - composer phpdoc - composer phpdoc [options] - composer dev-tools phpdoc -- [options] - vendor/bin/dev-tools phpdoc [options] + composer docheader + composer php-cs-fixer + composer docheader [options] + composer dev-tools docheader -- [options] + vendor/bin/dev-tools docheader [options] Arguments --------- @@ -61,25 +63,25 @@ Check PHPDocs (dry-run): .. code-block:: bash - composer phpdoc + composer docheader Fix PHPDocs automatically: .. code-block:: bash - composer phpdoc --fix + composer docheader --fix Check specific directory: .. code-block:: bash - composer phpdoc ./src + composer docheader ./src Check without cache: .. code-block:: bash - composer phpdoc --no-cache + composer docheader --no-cache Exit Codes --------- diff --git a/docs/commands/standards.rst b/docs/commands/standards.rst index 265cbdf4e..c5a843544 100644 --- a/docs/commands/standards.rst +++ b/docs/commands/standards.rst @@ -9,7 +9,7 @@ Description The ``standards`` command runs the full quality pipeline: 1. ``refactor`` - Rector code refactoring -2. ``phpdoc`` - PHPDoc checks and fixes +2. ``docheader`` - PHPDoc checks and fixes 3. ``code-style`` - Code style checking 4. ``reports`` - Documentation and test reports @@ -68,10 +68,10 @@ Behavior - Cache stays enabled by default for nested cache-aware phases; omit both flags to keep the command default, pass ``--cache`` to force it on, and pass ``--no-cache`` to force it off. -- The explicit cache intent is propagated to the nested ``phpdoc`` and +- The explicit cache intent is propagated to the nested ``docheader`` and ``reports`` phases. ``refactor`` and ``code-style`` do not consume this contract. -- When ``--cache-dir`` is provided, ``phpdoc`` and ``reports`` receive nested +- When ``--cache-dir`` is provided, ``docheader`` and ``reports`` receive nested cache directories under that base path. When it is omitted, each nested tool keeps its own default cache directory. - Progress output is disabled by default across nested phases; use diff --git a/docs/configuration/overriding-defaults.rst b/docs/configuration/overriding-defaults.rst index dfe50aaef..cf37dcf6e 100644 --- a/docs/configuration/overriding-defaults.rst +++ b/docs/configuration/overriding-defaults.rst @@ -38,7 +38,7 @@ Commands and Their Configuration Files * - ``dependencies`` - ``composer-dependency-analyser.php`` - Falls back to the packaged Composer Dependency Analyser configuration. - * - ``phpdoc`` + * - ``docheader`` - ``.php-cs-fixer.dist.php`` and ``rector.php`` - Falls back to the packaged files; ``.docheader`` is created locally when missing. @@ -62,7 +62,7 @@ A Practical Example ------------------- To customize Rector for one library, create ``rector.php`` in the consumer -project root. The ``refactor`` command and the Rector phase inside ``phpdoc`` +project root. The ``refactor`` command and the Rector phase inside ``docheader`` will use that file instead of the packaged default. Extending ECS Configuration diff --git a/docs/configuration/tooling-defaults.rst b/docs/configuration/tooling-defaults.rst index 950ae541b..051586318 100644 --- a/docs/configuration/tooling-defaults.rst +++ b/docs/configuration/tooling-defaults.rst @@ -14,16 +14,16 @@ create them on day one. - ``code-style`` - Fallback ECS configuration. * - ``rector.php`` - - ``refactor`` and ``phpdoc`` + - ``refactor`` and ``docheader`` - Fallback Rector configuration. * - ``phpunit.xml`` - ``tests`` - Registers ``FastForward\DevTools\PhpUnit\Runner\Extension\DevToolsExtension``. * - ``.php-cs-fixer.dist.php`` - - ``phpdoc`` + - ``docheader`` - Controls header and PHPDoc fixer behavior. * - ``.docheader`` - - ``phpdoc`` + - ``docheader`` - Created into the consumer root on demand when missing. * - ``.editorconfig`` - ``dev-tools:sync`` diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index ef78641f1..a35da6747 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -179,15 +179,15 @@ Important details: - ``--json`` and ``--pretty-json`` forward ``--output-format json`` to Rector and disable its progress bar. -``phpdoc`` ----------- +``docheader`` (alias: ``php-cs-fixer``) +--------------------------------------- Coordinates PHP-CS-Fixer and a focused Rector pass for missing method PHPDoc. .. code-block:: bash - composer phpdoc - composer phpdoc --fix + composer docheader + composer php-cs-fixer --fix Important details: diff --git a/docs/running/unified-command.rst b/docs/running/unified-command.rst index 1de2b07a5..909c05999 100644 --- a/docs/running/unified-command.rst +++ b/docs/running/unified-command.rst @@ -14,7 +14,7 @@ Execution Order ``standards`` runs these commands in sequence: 1. ``refactor`` -2. ``phpdoc`` +2. ``docheader`` 3. ``code-style`` 4. ``reports`` @@ -31,7 +31,7 @@ To allow the tools to modify files, use one of the following entry points: composer dev-tools:fix vendor/bin/dev-tools --fix -The flag mainly affects ``refactor``, ``phpdoc``, and ``code-style``. The +The flag mainly affects ``refactor``, ``docheader``, and ``code-style``. The reporting steps still run, but they do not use the flag themselves. When the Unified Command Is the Right Choice diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 4dd33ab0f..4b049ba9f 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -238,7 +238,7 @@ Recovery: .. code-block:: bash - composer dev-tools phpdoc + composer dev-tools docheader composer dev-tools docs Fix source PHPDoc first, then regenerate docs. Avoid editing generated API diff --git a/docs/usage/common-workflows.rst b/docs/usage/common-workflows.rst index cd2c7a103..fd0b96dfe 100644 --- a/docs/usage/common-workflows.rst +++ b/docs/usage/common-workflows.rst @@ -11,7 +11,7 @@ Most day-to-day work falls into one of the flows below. - What happens * - Check everything before a pull request - ``composer dev-tools`` - - Runs ``refactor``, ``phpdoc``, ``code-style``, and ``reports`` in + - Runs ``refactor``, ``docheader``, ``code-style``, and ``reports`` in order. * - Auto-fix what can be changed safely - ``composer dev-tools:fix`` diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 7b2ab9232..61a25848b 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -44,8 +44,8 @@ * Provides operations to inspect, lint, and repair PHPDoc comments across the project. * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ -#[AsCommand(name: 'standards:dockblock', description: 'Checks and fixes PHPDocs.', aliases: [ - 'dockblock', +#[AsCommand(name: 'standards:docheader', description: 'Checks and fixes PHPDocs.', aliases: [ + 'docheader', 'php-cs-fixer', ])] final class PhpDocCommand extends Command diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index 0b141afa3..b2b7aa678 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -181,7 +181,7 @@ private function getProcessLabel(string $command): string { return match ($command) { 'refactor' => 'Refactoring Code with DevTools', - 'php-cs-fixer', 'dockblock' => 'Checking PHPDoc with DevTools', + 'php-cs-fixer', 'docheader' => 'Checking PHPDoc with DevTools', 'code-style' => 'Checking Code Style with DevTools', 'reports' => 'Generating Reports with DevTools', default => 'Running DevTools Command', From 545461938e86be6adc6d13e311678b39be10e850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 18:27:28 -0300 Subject: [PATCH 24/31] chore: normalize phpdoc command naming and docs --- CHANGELOG.md | 6 +++-- README.md | 2 +- docs/advanced/rector-and-phpdoc.rst | 10 ++++---- docs/api/commands.rst | 4 ++-- docs/commands/phpdoc.rst | 23 +++++++++---------- docs/commands/standards.rst | 6 ++--- docs/configuration/overriding-defaults.rst | 4 ++-- docs/configuration/tooling-defaults.rst | 6 ++--- docs/running/specialized-commands.rst | 4 ++-- docs/running/unified-command.rst | 4 ++-- docs/troubleshooting.rst | 2 +- docs/usage/common-workflows.rst | 2 +- src/Console/Command/DocsCommand.php | 2 +- src/Console/Command/PhpDocCommand.php | 3 ++- src/Console/Command/StandardsCommand.php | 8 +++---- .../Console/Command/StandardsCommandTest.php | 6 ++--- 16 files changed, 47 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5c5555a..ca254b52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Align the standards pipeline to invoke PHPDoc checks via the `php-cs-fixer` command name/alias and route standards cache to `.dev-tools/cache/php-cs-fixer`, preserving backwards compatibility for existing `docheader`-style entry points. -- Rename the command entry point from `standards:phpdoc` to `standards:docheader` and keep both `docheader` and `php-cs-fixer` aliases for compatibility (`standards:phpdoc` is intentionally not kept as a command name). +- Restore `standards:phpdoc` as the command entry point for PHPDoc checks while keeping + the legacy aliases `docheader` and `php-cs-fixer`. +- Remove the `phpdoc` alias from `reports:docs` to avoid command-name collisions with + the standards PHPDoc check command. ## [1.22.3] - 2026-04-25 diff --git a/README.md b/README.md index f95321b6f..fb441f6cd 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ composer code-style composer refactor # Check and fix PHPDoc comments -composer docheader +composer phpdoc # Generate HTML API documentation using phpDocumentor composer docs diff --git a/docs/advanced/rector-and-phpdoc.rst b/docs/advanced/rector-and-phpdoc.rst index c357dae4a..f67287bed 100644 --- a/docs/advanced/rector-and-phpdoc.rst +++ b/docs/advanced/rector-and-phpdoc.rst @@ -1,14 +1,14 @@ -Rector and Docheader Automation +Rector and PHPDoc Automation =============================== The package uses two different Rector entry points, and that difference matters when you are trying to understand why a rule did or did not run. -``refactor`` Versus ``docheader`` +``refactor`` Versus ``phpdoc`` ------------------------------ - ``refactor`` uses the full ``rector.php`` file. -- ``docheader`` runs PHP-CS-Fixer first and then executes Rector with +- ``phpdoc`` runs PHP-CS-Fixer first and then executes Rector with ``--only \FastForward\DevTools\Rector\AddMissingMethodPhpDocRector``. Rules Shipped by the Package @@ -19,7 +19,7 @@ Rules Shipped by the Package * - Rule - Enabled in packaged ``rector.php`` - - Used directly by ``docheader`` + - Used directly by ``phpdoc`` - Purpose * - ``FastForward\DevTools\Rector\AddMissingMethodPhpDocRector`` - Yes @@ -45,7 +45,7 @@ rules when ``thecodingmachine/safe`` is installed. Why ``.docheader`` Appears Automatically ---------------------------------------- -The ``docheader`` command creates ``.docheader`` in the consumer root when it is +The ``phpdoc`` command creates ``.docheader`` in the consumer root when it is missing. The template comes from the packaged file and the package name is rewritten to match the current project whenever Composer metadata is available. diff --git a/docs/api/commands.rst b/docs/api/commands.rst index 8b271405e..6fa37a06b 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -37,9 +37,9 @@ subprocess execution is needed. - ``refactor`` - Runs Rector with local or packaged configuration. * - ``FastForward\DevTools\Console\Command\PhpDocCommand`` - - ``standards:docheader`` + - ``standards:phpdoc`` - Runs PHP-CS-Fixer and a focused Rector PHPDoc pass. Supported aliases: - ``docheader`` and ``php-cs-fixer``. + ``phpdoc``, ``docheader`` and ``php-cs-fixer``. * - ``FastForward\DevTools\Console\Command\CodeStyleCommand`` - ``code-style`` - Runs Composer Normalize and ECS. diff --git a/docs/commands/phpdoc.rst b/docs/commands/phpdoc.rst index 9ff83e4e0..47f1c28c2 100644 --- a/docs/commands/phpdoc.rst +++ b/docs/commands/phpdoc.rst @@ -1,12 +1,12 @@ -docheader -========= +phpdoc +====== Checks and fixes PHPDoc comments. Description ----------- -The ``docheader`` command coordinates PHPDoc checking and fixing using: +The ``phpdoc`` command coordinates PHPDoc checking and fixing using: (alias ``docheader`` and ``php-cs-fixer``) - PHP-CS-Fixer - fixes PHPDoc formatting @@ -19,11 +19,10 @@ Usage .. code-block:: bash - composer docheader - composer php-cs-fixer - composer docheader [options] - composer dev-tools docheader -- [options] - vendor/bin/dev-tools docheader [options] + composer phpdoc + composer phpdoc [options] + composer dev-tools phpdoc -- [options] + vendor/bin/dev-tools phpdoc [options] Arguments --------- @@ -63,25 +62,25 @@ Check PHPDocs (dry-run): .. code-block:: bash - composer docheader + composer phpdoc Fix PHPDocs automatically: .. code-block:: bash - composer docheader --fix + composer phpdoc --fix Check specific directory: .. code-block:: bash - composer docheader ./src + composer phpdoc ./src Check without cache: .. code-block:: bash - composer docheader --no-cache + composer phpdoc --no-cache Exit Codes --------- diff --git a/docs/commands/standards.rst b/docs/commands/standards.rst index c5a843544..265cbdf4e 100644 --- a/docs/commands/standards.rst +++ b/docs/commands/standards.rst @@ -9,7 +9,7 @@ Description The ``standards`` command runs the full quality pipeline: 1. ``refactor`` - Rector code refactoring -2. ``docheader`` - PHPDoc checks and fixes +2. ``phpdoc`` - PHPDoc checks and fixes 3. ``code-style`` - Code style checking 4. ``reports`` - Documentation and test reports @@ -68,10 +68,10 @@ Behavior - Cache stays enabled by default for nested cache-aware phases; omit both flags to keep the command default, pass ``--cache`` to force it on, and pass ``--no-cache`` to force it off. -- The explicit cache intent is propagated to the nested ``docheader`` and +- The explicit cache intent is propagated to the nested ``phpdoc`` and ``reports`` phases. ``refactor`` and ``code-style`` do not consume this contract. -- When ``--cache-dir`` is provided, ``docheader`` and ``reports`` receive nested +- When ``--cache-dir`` is provided, ``phpdoc`` and ``reports`` receive nested cache directories under that base path. When it is omitted, each nested tool keeps its own default cache directory. - Progress output is disabled by default across nested phases; use diff --git a/docs/configuration/overriding-defaults.rst b/docs/configuration/overriding-defaults.rst index cf37dcf6e..dfe50aaef 100644 --- a/docs/configuration/overriding-defaults.rst +++ b/docs/configuration/overriding-defaults.rst @@ -38,7 +38,7 @@ Commands and Their Configuration Files * - ``dependencies`` - ``composer-dependency-analyser.php`` - Falls back to the packaged Composer Dependency Analyser configuration. - * - ``docheader`` + * - ``phpdoc`` - ``.php-cs-fixer.dist.php`` and ``rector.php`` - Falls back to the packaged files; ``.docheader`` is created locally when missing. @@ -62,7 +62,7 @@ A Practical Example ------------------- To customize Rector for one library, create ``rector.php`` in the consumer -project root. The ``refactor`` command and the Rector phase inside ``docheader`` +project root. The ``refactor`` command and the Rector phase inside ``phpdoc`` will use that file instead of the packaged default. Extending ECS Configuration diff --git a/docs/configuration/tooling-defaults.rst b/docs/configuration/tooling-defaults.rst index 051586318..950ae541b 100644 --- a/docs/configuration/tooling-defaults.rst +++ b/docs/configuration/tooling-defaults.rst @@ -14,16 +14,16 @@ create them on day one. - ``code-style`` - Fallback ECS configuration. * - ``rector.php`` - - ``refactor`` and ``docheader`` + - ``refactor`` and ``phpdoc`` - Fallback Rector configuration. * - ``phpunit.xml`` - ``tests`` - Registers ``FastForward\DevTools\PhpUnit\Runner\Extension\DevToolsExtension``. * - ``.php-cs-fixer.dist.php`` - - ``docheader`` + - ``phpdoc`` - Controls header and PHPDoc fixer behavior. * - ``.docheader`` - - ``docheader`` + - ``phpdoc`` - Created into the consumer root on demand when missing. * - ``.editorconfig`` - ``dev-tools:sync`` diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index a35da6747..d875db208 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -179,14 +179,14 @@ Important details: - ``--json`` and ``--pretty-json`` forward ``--output-format json`` to Rector and disable its progress bar. -``docheader`` (alias: ``php-cs-fixer``) +``phpdoc`` (alias: ``docheader`` and ``php-cs-fixer``) --------------------------------------- Coordinates PHP-CS-Fixer and a focused Rector pass for missing method PHPDoc. .. code-block:: bash - composer docheader + composer phpdoc composer php-cs-fixer --fix Important details: diff --git a/docs/running/unified-command.rst b/docs/running/unified-command.rst index 909c05999..1de2b07a5 100644 --- a/docs/running/unified-command.rst +++ b/docs/running/unified-command.rst @@ -14,7 +14,7 @@ Execution Order ``standards`` runs these commands in sequence: 1. ``refactor`` -2. ``docheader`` +2. ``phpdoc`` 3. ``code-style`` 4. ``reports`` @@ -31,7 +31,7 @@ To allow the tools to modify files, use one of the following entry points: composer dev-tools:fix vendor/bin/dev-tools --fix -The flag mainly affects ``refactor``, ``docheader``, and ``code-style``. The +The flag mainly affects ``refactor``, ``phpdoc``, and ``code-style``. The reporting steps still run, but they do not use the flag themselves. When the Unified Command Is the Right Choice diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 4b049ba9f..4dd33ab0f 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -238,7 +238,7 @@ Recovery: .. code-block:: bash - composer dev-tools docheader + composer dev-tools phpdoc composer dev-tools docs Fix source PHPDoc first, then regenerate docs. Avoid editing generated API diff --git a/docs/usage/common-workflows.rst b/docs/usage/common-workflows.rst index fd0b96dfe..cd2c7a103 100644 --- a/docs/usage/common-workflows.rst +++ b/docs/usage/common-workflows.rst @@ -11,7 +11,7 @@ Most day-to-day work falls into one of the flows below. - What happens * - Check everything before a pull request - ``composer dev-tools`` - - Runs ``refactor``, ``docheader``, ``code-style``, and ``reports`` in + - Runs ``refactor``, ``phpdoc``, ``code-style``, and ``reports`` in order. * - Auto-fix what can be changed safely - ``composer dev-tools:fix`` diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index eb3d8345d..5cc9a460b 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -49,7 +49,7 @@ #[AsCommand( name: 'reports:docs', description: 'Generates API documentation.', - aliases: ['reports:phpdoc', 'phpdoc', 'phpDocumentor', 'docs'], + aliases: ['reports:phpdoc', 'phpDocumentor', 'docs'], )] final class DocsCommand extends Command { diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 61a25848b..579defb9f 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -44,7 +44,8 @@ * Provides operations to inspect, lint, and repair PHPDoc comments across the project. * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ -#[AsCommand(name: 'standards:docheader', description: 'Checks and fixes PHPDocs.', aliases: [ +#[AsCommand(name: 'standards:phpdoc', description: 'Checks and fixes PHPDocs.', aliases: [ + 'phpdoc', 'docheader', 'php-cs-fixer', ])] diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index b2b7aa678..b8cbcca46 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -113,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'input' => $input, ]); - foreach (['refactor', 'php-cs-fixer', 'code-style', 'reports'] as $command) { + foreach (['refactor', 'phpdoc', 'code-style', 'reports'] as $command) { $commands[] = $command; $processBuilder = $this->processBuilder; @@ -129,13 +129,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $processBuilder = $processBuilder->withArgument('--fix'); } - if (\in_array($command, ['php-cs-fixer', 'reports'], true) && null !== $cacheArgument) { + if (\in_array($command, ['phpdoc', 'reports'], true) && null !== $cacheArgument) { $processBuilder = $processBuilder->withArgument($cacheArgument); } if ( $cacheDirEnabled - && \in_array($command, ['php-cs-fixer', 'reports'], true) + && \in_array($command, ['phpdoc', 'reports'], true) && null !== $cacheDir = $this->resolveCacheDirArgument($input, $command) ) { $processBuilder = $processBuilder->withArgument('--cache-dir', $cacheDir); @@ -181,7 +181,7 @@ private function getProcessLabel(string $command): string { return match ($command) { 'refactor' => 'Refactoring Code with DevTools', - 'php-cs-fixer', 'docheader' => 'Checking PHPDoc with DevTools', + 'phpdoc', 'docheader', 'php-cs-fixer' => 'Checking PHPDoc with DevTools', 'code-style' => 'Checking Code Style with DevTools', 'reports' => 'Generating Reports with DevTools', default => 'Running DevTools Command', diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index 764fa6bc4..9ebab4d62 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -118,7 +118,7 @@ public function executeWillRunSuiteSequentially(): void 'Code standards checks completed successfully.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface && $context['output'] instanceof OutputInterface - && ['refactor', 'php-cs-fixer', 'code-style', 'reports'] === $context['commands']), + && ['refactor', 'phpdoc', 'code-style', 'reports'] === $context['commands']), )->shouldBeCalled(); self::assertSame(StandardsCommand::SUCCESS, $this->invokeExecute()); @@ -143,7 +143,7 @@ public function executeWillReturnFailureWhenAnyCommandFails(): void 'Code standards checks failed.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface && $context['output'] instanceof OutputInterface - && ['refactor', 'php-cs-fixer', 'code-style', 'reports'] === $context['commands']), + && ['refactor', 'phpdoc', 'code-style', 'reports'] === $context['commands']), )->shouldBeCalled(); self::assertSame(StandardsCommand::FAILURE, $this->invokeExecute()); @@ -162,7 +162,7 @@ public function executeWithCacheWillForwardCacheOnlyToCacheAwareNestedCommands() $this->processBuilder->withArgument('--cache') ->willReturn($this->processBuilder->reveal()) ->shouldBeCalledTimes(2); - $this->processBuilder->withArgument('--cache-dir', '.dev-tools/cache/php-cs-fixer') + $this->processBuilder->withArgument('--cache-dir', '.dev-tools/cache/phpdoc') ->willReturn($this->processBuilder->reveal()) ->shouldBeCalledOnce(); $this->processBuilder->withArgument('--cache-dir', '.dev-tools/cache/reports') From fe313cc27c3541e9d20343faabc9a5edcc2a18aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 18:33:52 -0300 Subject: [PATCH 25/31] docs: fix heading formatting in rector-and-phpdoc.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- docs/advanced/rector-and-phpdoc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/rector-and-phpdoc.rst b/docs/advanced/rector-and-phpdoc.rst index f67287bed..27e80d883 100644 --- a/docs/advanced/rector-and-phpdoc.rst +++ b/docs/advanced/rector-and-phpdoc.rst @@ -1,5 +1,5 @@ Rector and PHPDoc Automation -=============================== +============================ The package uses two different Rector entry points, and that difference matters when you are trying to understand why a rule did or did not run. From 6c52eb547af6fd1754c97d0be8187d0587b9bcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 19:04:11 -0300 Subject: [PATCH 26/31] test: centralize tooling excluded directories and cleanup fixture handling --- .agents/skills/phpunit-tests/SKILL.md | 53 +++++++++++++++ .php-cs-fixer.dist.php | 10 +-- docs/commands/metrics.rst | 2 +- src/Console/Command/MetricsCommand.php | 2 +- src/Path/WorkingProjectPathResolver.php | 51 +++++++++++---- tests/Console/Command/MetricsCommandTest.php | 2 +- .../composer-plugin-consumer/.gitignore | 12 ---- .../composer-plugin-consumer/composer.json | 38 ----------- tests/Path/WorkingProjectPathResolverTest.php | 64 ++++++++++++++++--- 9 files changed, 153 insertions(+), 81 deletions(-) delete mode 100644 tests/Fixtures/composer-plugin-consumer/.gitignore delete mode 100644 tests/Fixtures/composer-plugin-consumer/composer.json diff --git a/.agents/skills/phpunit-tests/SKILL.md b/.agents/skills/phpunit-tests/SKILL.md index f568918b0..57218971f 100644 --- a/.agents/skills/phpunit-tests/SKILL.md +++ b/.agents/skills/phpunit-tests/SKILL.md @@ -83,4 +83,57 @@ Choose the smallest command that proves the change. - Re-run after fixes until the targeted tests pass. - If failures come from unrelated pre-existing breakage, call that out separately and do not silently claim success. +### Composer plugin validation flow + +Some repository test scenarios are only valid when DevTools is executed as a Composer plugin. +Create a temporary fixture under `backup/` before running plugin-based verification. + +```bash +PROJECT_ROOT="$(pwd)" +PLUGIN_FIXTURE="$PROJECT_ROOT/backup/composer-plugin-consumer" +mkdir -p "$PROJECT_ROOT/backup" +rm -rf "$PLUGIN_FIXTURE" +mkdir -p "$PLUGIN_FIXTURE" + +cat > "$PLUGIN_FIXTURE/composer.json" <<'JSON' +{ + "name": "fast-forward/dev-tools-composer-plugin-consumer-fixture", + "description": "Fixture project used to validate DevTools plugin behavior.", + "license": "MIT", + "type": "project", + "require": { + "fast-forward/dev-tools": "*@dev" + }, + "repositories": [ + { + "type": "path", + "url": "../..", + "options": { + "symlink": true + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "fast-forward/dev-tools": true, + "phpdocumentor/shim": true, + "phpro/grumphp-shim": true, + "pyrech/composer-changelogs": true + } + }, + "scripts": { + "dev-tools": "dev-tools", + "dev-tools:fix": "@dev-tools --fix" + } +} +JSON + +cd "$PLUGIN_FIXTURE" +composer install +composer tests +``` + Read [references/generation-checklist.md](references/generation-checklist.md) before writing or repairing tests in an unfamiliar repository. diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index af8cc07d7..01a76f7b7 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,6 +2,10 @@ declare(strict_types=1); +require __DIR__ . '/vendor/autoload.php'; + +use FastForward\DevTools\Path\WorkingProjectPathResolver; + $rules = [ 'phpdoc_indent' => true, 'phpdoc_order' => [ @@ -39,11 +43,7 @@ $finder = PhpCsFixer\Finder::create() ->in([getcwd()]) - ->exclude('public') - ->exclude('resources') - ->exclude('vendor') - ->exclude('tmp') -; + ->exclude(WorkingProjectPathResolver::TOOLING_EXCLUDED_DIRECTORIES); return (new PhpCsFixer\Config()) ->setRiskyAllowed(false) diff --git a/docs/commands/metrics.rst b/docs/commands/metrics.rst index 0005663d2..c7ca5794b 100644 --- a/docs/commands/metrics.rst +++ b/docs/commands/metrics.rst @@ -33,7 +33,7 @@ Options Comma-separated directories that should be excluded from analysis. Default: - ``vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures``. + ``vendor,tmp,cache,spec,build,.dev-tools,backup,resources``. ``--target=`` Output directory for the generated metrics reports. diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 0758a7fca..434dbf620 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -89,7 +89,7 @@ protected function configure(): void name: 'exclude', mode: InputOption::VALUE_OPTIONAL, description: 'Comma-separated directories that SHOULD be excluded from analysis.', - default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures', + default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources', ) ->addOption( name: 'target', diff --git a/src/Path/WorkingProjectPathResolver.php b/src/Path/WorkingProjectPathResolver.php index df013c6fb..e2b7716ea 100644 --- a/src/Path/WorkingProjectPathResolver.php +++ b/src/Path/WorkingProjectPathResolver.php @@ -29,6 +29,23 @@ */ final class WorkingProjectPathResolver { + /** + * @var list repository-local directories ignored by tooling + */ + public const array TOOLING_EXCLUDED_DIRECTORIES = [ + '.dev-tools', + 'backup', + 'cache', + 'public', + 'resources', + 'tmp', + 'vendor', + '*/vendor', + '*/vendor/*', + '**/vendor', + '**/vendor/*', + ]; + /** * Returns the current working project directory or a path under it. * @@ -52,19 +69,27 @@ public static function getProjectPath(string $path = ''): string */ public static function getToolingExcludedDirectories(string $baseDir = ''): array { - return [ - ManagedWorkspace::getOutputDirectory(baseDir: $baseDir), - Path::join($baseDir, 'backup'), - Path::join($baseDir, 'cache'), - Path::join($baseDir, 'public'), - Path::join($baseDir, 'resources'), - Path::join($baseDir, 'tmp'), - Path::join($baseDir, 'vendor'), - Path::join($baseDir, '*/vendor'), - Path::join($baseDir, '*/vendor/*'), - Path::join($baseDir, '**/vendor'), - Path::join($baseDir, '**/vendor/*'), + $excludeFromBaseDir = [ + '.dev-tools' => ManagedWorkspace::getOutputDirectory(baseDir: $baseDir), + 'backup' => Path::join($baseDir, 'backup'), + 'cache' => Path::join($baseDir, 'cache'), + 'public' => Path::join($baseDir, 'public'), + 'resources' => Path::join($baseDir, 'resources'), + 'tmp' => Path::join($baseDir, 'tmp'), + 'vendor' => Path::join($baseDir, 'vendor'), + '*/vendor' => Path::join($baseDir, '*/vendor'), + '*/vendor/*' => Path::join($baseDir, '*/vendor/*'), + '**/vendor' => Path::join($baseDir, '**/vendor'), + '**/vendor/*' => Path::join($baseDir, '**/vendor/*'), ]; + + $directories = []; + + foreach (self::TOOLING_EXCLUDED_DIRECTORIES as $excludedDirectory) { + $directories[] = $excludeFromBaseDir[$excludedDirectory]; + } + + return $directories; } /** @@ -81,7 +106,7 @@ public static function getToolingSourcePaths(string $baseDir = ''): array ->files() ->name('*.php') ->in($workingDirectory) - ->exclude(['.dev-tools', 'backup', 'cache', 'public', 'resources', 'tmp', 'vendor']) + ->exclude(self::TOOLING_EXCLUDED_DIRECTORIES) ->sortByName(); $paths = []; diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index b7b5775fb..1737dde21 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -73,7 +73,7 @@ protected function setUp(): void $this->process = $this->prophesize(Process::class); $this->input->getOption('exclude') - ->willReturn('vendor,tests/Fixtures'); + ->willReturn('vendor'); $this->input->getOption('target') ->willReturn(ManagedWorkspace::getOutputDirectory(ManagedWorkspace::METRICS)); $this->input->getOption('junit') diff --git a/tests/Fixtures/composer-plugin-consumer/.gitignore b/tests/Fixtures/composer-plugin-consumer/.gitignore deleted file mode 100644 index 3e76ade8a..000000000 --- a/tests/Fixtures/composer-plugin-consumer/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -.dev-tools/ -.idea/ -.vscode/ -backup/ -tmp/ -vendor/ -!.gitignore -!composer.json -* -*.cache -.DS_Store -composer.lock diff --git a/tests/Fixtures/composer-plugin-consumer/composer.json b/tests/Fixtures/composer-plugin-consumer/composer.json deleted file mode 100644 index 1919878a9..000000000 --- a/tests/Fixtures/composer-plugin-consumer/composer.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "fast-forward/dev-tools-composer-plugin-consumer-fixture", - "description": "Fixture project used to verify DevTools Composer plugin consumer behavior.", - "license": "MIT", - "type": "project", - "require": { - "fast-forward/dev-tools": "*@dev" - }, - "repositories": [ - { - "type": "path", - "url": "../../..", - "options": { - "symlink": true - } - } - ], - "minimum-stability": "dev", - "prefer-stable": true, - "config": { - "allow-plugins": { - "ergebnis/composer-normalize": true, - "fast-forward/dev-tools": true, - "phpdocumentor/shim": true, - "phpro/grumphp-shim": true, - "pyrech/composer-changelogs": true - } - }, - "extra": { - "grumphp": { - "config-default-path": "../../../grumphp.yml" - } - }, - "scripts": { - "dev-tools": "dev-tools", - "dev-tools:fix": "@dev-tools --fix" - } -} diff --git a/tests/Path/WorkingProjectPathResolverTest.php b/tests/Path/WorkingProjectPathResolverTest.php index c872e5f24..7494048b3 100644 --- a/tests/Path/WorkingProjectPathResolverTest.php +++ b/tests/Path/WorkingProjectPathResolverTest.php @@ -26,11 +26,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use function Safe\scandir; +use function Safe\rmdir; +use function Safe\unlink; use function Safe\file_put_contents; use function Safe\getcwd; use function Safe\mkdir; use function Safe\realpath; -use function sys_get_temp_dir; use function uniqid; #[CoversClass(WorkingProjectPathResolver::class)] @@ -113,9 +115,9 @@ public function itWillExposeRelativeToolingSkipPatternsByDefault(): void * @return void */ #[Test] - public function itWillExposeToolingSourcePathsWithoutTraversingVendorDirectories(): void + public function itWillExposeToolingSourcePathsIgnoringExcludedDirectories(): void { - $fixtureDirectory = sys_get_temp_dir() . '/dev-tools-path-resolver-' . uniqid(); + $fixtureDirectory = \dirname(__DIR__, 2) . '/backup/dev-tools-path-resolver-' . uniqid(); mkdir($fixtureDirectory . '/src', recursive: true); mkdir($fixtureDirectory . '/tests/Fixtures/consumer/vendor/package/src', recursive: true); @@ -130,13 +132,55 @@ public function itWillExposeToolingSourcePathsWithoutTraversingVendorDirectories file_put_contents($fixtureDirectory . '/backup/Backup.php', ' Date: Sun, 26 Apr 2026 22:06:06 +0000 Subject: [PATCH 27/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 219a9117b..a366131f5 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 219a9117ba3404bce788febb2d3d60cb03b40155 +Subproject commit a366131f5b251c376c82ace840c9d9926bc5ff1c From 0e1f782a996f54df545a3cf70aff50e88b573c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 19:07:33 -0300 Subject: [PATCH 28/31] feat: add ignoreErrorsOnPackage for 'composer/composer' in configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Signed-off-by: Felipe Sayão Lobato Abreu --- src/Config/ComposerDependencyAnalyserConfig.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Config/ComposerDependencyAnalyserConfig.php b/src/Config/ComposerDependencyAnalyserConfig.php index a0f88b476..f62f95f4b 100644 --- a/src/Config/ComposerDependencyAnalyserConfig.php +++ b/src/Config/ComposerDependencyAnalyserConfig.php @@ -152,6 +152,7 @@ public static function applyPackagedRepositoryIgnores(Configuration $configurati self::DEFAULT_PACKAGED_PROD_ONLY_IN_DEV_DEPENDENCIES, [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV], ); + $configuration->ignoreErrorsOnPackage('composer/composer', [ErrorType::DEV_DEPENDENCY_IN_PROD]); return $configuration; } From e41921f195b2a0905ebfc442ef62a121f695eae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 19:08:55 -0300 Subject: [PATCH 29/31] docs: update phpdoc section to clarify aliases and functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- docs/running/specialized-commands.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index d875db208..ef78641f1 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -179,15 +179,15 @@ Important details: - ``--json`` and ``--pretty-json`` forward ``--output-format json`` to Rector and disable its progress bar. -``phpdoc`` (alias: ``docheader`` and ``php-cs-fixer``) ---------------------------------------- +``phpdoc`` +---------- Coordinates PHP-CS-Fixer and a focused Rector pass for missing method PHPDoc. .. code-block:: bash composer phpdoc - composer php-cs-fixer --fix + composer phpdoc --fix Important details: From 67557a6a5b0fd978f518472f6e7be4fd530f3874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Sun, 26 Apr 2026 19:20:36 -0300 Subject: [PATCH 30/31] docs: update parameter descriptions for SymfonyStyle in command classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/Command/CodeOwnersCommand.php | 2 +- src/Console/Command/CopyResourceCommand.php | 2 +- src/Console/Command/FundingCommand.php | 2 +- src/Console/Command/GitAttributesCommand.php | 4 ++-- src/Console/Command/GitHooksCommand.php | 4 ++-- src/Console/Command/GitIgnoreCommand.php | 4 ++-- src/Console/Command/LicenseCommand.php | 4 ++-- src/Console/Command/StandardsCommand.php | 2 +- .../Command/UpdateComposerJsonCommand.php | 4 ++-- src/Path/WorkingProjectPathResolver.php | 16 +--------------- src/ServiceProvider/DevToolsServiceProvider.php | 2 -- 11 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/Console/Command/CodeOwnersCommand.php b/src/Console/Command/CodeOwnersCommand.php index bf46812af..669109f13 100644 --- a/src/Console/Command/CodeOwnersCommand.php +++ b/src/Console/Command/CodeOwnersCommand.php @@ -54,7 +54,7 @@ final class CodeOwnersCommand extends Command * @param FilesystemInterface $filesystem the filesystem used to read and write the target file * @param FileDiffer $fileDiffer the differ used to report managed-file drift * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the SymfonyStyle instance for interactive prompts */ public function __construct( private readonly CodeOwnersGenerator $generator, diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 18e9cd287..950a485df 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -57,7 +57,7 @@ final class CopyResourceCommand extends Command * @param FinderFactoryInterface $finderFactory the factory used to create finders for directory resources * @param FileDiffer $fileDiffer the service used to summarize overwrite changes * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly FilesystemInterface $filesystem, diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index f4e4e935f..966e487f8 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -61,7 +61,7 @@ final class FundingCommand extends Command * @param ProcessBuilderInterface $processBuilder the process builder used to normalize composer.json after updates * @param ProcessQueueInterface $processQueue the process queue used to execute composer normalize * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly FilesystemInterface $filesystem, diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index f453f0fb0..fe4fec7f9 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -76,9 +76,9 @@ final class GitAttributesCommand extends Command * @param WriterInterface $writer the writer component * @param FilesystemInterface $filesystem the filesystem component * @param ComposerJsonInterface $composer the composer.json accessor - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly CandidateProviderInterface $candidateProvider, diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index 3dd87cbf8..be98f4912 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -55,9 +55,9 @@ final class GitHooksCommand extends Command * @param FilesystemInterface $filesystem the filesystem used to copy hooks * @param FileLocatorInterface $fileLocator the locator used to find packaged hooks * @param FinderFactoryInterface $finderFactory the factory used to create finders for hook files - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly FilesystemInterface $filesystem, diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index bc4971fb8..57bc7072e 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -68,9 +68,9 @@ final class GitIgnoreCommand extends Command * @param ReaderInterface $reader the reader component * @param WriterInterface|null $writer the writer component * @param FileLocatorInterface $fileLocator the file locator - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly MergerInterface $merger, diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index beb0342e1..94314345b 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -54,9 +54,9 @@ final class LicenseCommand extends Command * * @param GeneratorInterface $generator the generator component * @param FilesystemInterface $filesystem the filesystem component - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly GeneratorInterface $generator, diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index b8cbcca46..c2dd4c221 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -181,7 +181,7 @@ private function getProcessLabel(string $command): string { return match ($command) { 'refactor' => 'Refactoring Code with DevTools', - 'phpdoc', 'docheader', 'php-cs-fixer' => 'Checking PHPDoc with DevTools', + 'phpdoc' => 'Checking PHPDoc with DevTools', 'code-style' => 'Checking Code Style with DevTools', 'reports' => 'Generating Reports with DevTools', default => 'Running DevTools Command', diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index b8c95b7ac..02aff4cf5 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -60,9 +60,9 @@ final class UpdateComposerJsonCommand extends Command * @param ComposerJsonInterface $composer the composer.json metadata accessor * @param FilesystemInterface $filesystem the filesystem used to read and write composer.json * @param FileLocatorInterface $fileLocator the locator used to resolve packaged configuration files - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger - * @param SymfonyStyle $io + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly ComposerJsonInterface $composer, diff --git a/src/Path/WorkingProjectPathResolver.php b/src/Path/WorkingProjectPathResolver.php index e2b7716ea..cd4258941 100644 --- a/src/Path/WorkingProjectPathResolver.php +++ b/src/Path/WorkingProjectPathResolver.php @@ -69,24 +69,10 @@ public static function getProjectPath(string $path = ''): string */ public static function getToolingExcludedDirectories(string $baseDir = ''): array { - $excludeFromBaseDir = [ - '.dev-tools' => ManagedWorkspace::getOutputDirectory(baseDir: $baseDir), - 'backup' => Path::join($baseDir, 'backup'), - 'cache' => Path::join($baseDir, 'cache'), - 'public' => Path::join($baseDir, 'public'), - 'resources' => Path::join($baseDir, 'resources'), - 'tmp' => Path::join($baseDir, 'tmp'), - 'vendor' => Path::join($baseDir, 'vendor'), - '*/vendor' => Path::join($baseDir, '*/vendor'), - '*/vendor/*' => Path::join($baseDir, '*/vendor/*'), - '**/vendor' => Path::join($baseDir, '**/vendor'), - '**/vendor/*' => Path::join($baseDir, '**/vendor/*'), - ]; - $directories = []; foreach (self::TOOLING_EXCLUDED_DIRECTORIES as $excludedDirectory) { - $directories[] = $excludeFromBaseDir[$excludedDirectory]; + $directories[] = Path::join($baseDir, $excludedDirectory); } return $directories; diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index ec349aeec..caba841fe 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -35,7 +35,6 @@ use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use FastForward\DevTools\Console\Formatter\LogLevelOutputFormatter; -use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Console\Logger\OutputFormatLogger; use FastForward\DevTools\Console\Logger\Processor\CommandInputProcessor; use FastForward\DevTools\Console\Logger\Processor\CommandOutputProcessor; @@ -164,7 +163,6 @@ public function getFactories(): array InputInterface::class => get(ArgvInput::class), OutputInterface::class => get(ConsoleOutputInterface::class), CommandLoaderInterface::class => get(DevToolsCommandLoader::class), - DevTools::class => create(DevTools::class)->constructor(get(DevToolsCommandLoader::class)), CommandProvider::class => get(DevToolsCommandProvider::class), ConsoleOutputInterface::class => create(ConsoleOutput::class) ->method('setVerbosity', ConsoleOutputInterface::VERBOSITY_VERBOSE) From eebe5eca7033ae03a6fc6bdebed10267a79a0547 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:22:30 +0000 Subject: [PATCH 31/31] Update wiki submodule pointer for PR #270 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index a366131f5..c99c7cd82 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit a366131f5b251c376c82ace840c9d9926bc5ff1c +Subproject commit c99c7cd826a90dc2aa8c85cfe7dd05b4186904c4