From d4b4a06cd61df349a148ace5e511e5a711c87ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:34:36 -0300 Subject: [PATCH 1/9] [command] Add global dev-tools no-logo control for nested runs --- CHANGELOG.md | 4 ++ src/Console/DevTools.php | 17 +++++-- src/Process/ProcessBuilder.php | 53 ++++++++++++++++++++++ tests/Console/DevToolsTest.php | 67 ++++++++++++++++++++++++++++ tests/Process/ProcessBuilderTest.php | 45 +++++++++++++++++++ 5 files changed, 183 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2420afefbf..1e0555a73e 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] +### Fixed + +- Hide DevTools logo in nested subprocesses by adding a new `--no-logo` global option for the top-level application and automatically attaching it to internal DevTools child processes, avoiding banner repetition in orchestrated command queues (#277) + ### Added - Add a configurable DevTools generated artifact workspace through `--workspace-dir` and `FAST_FORWARD_WORKSPACE_DIR`, keeping explicit output/cache command options authoritative (#274) diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 14edf2df7a..234c88314d 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -47,6 +47,8 @@ */ final class DevTools extends Application { + private const string NO_LOGO_OPTION = 'no-logo'; + private const string LOGO = <<<'LOGO' ____ _____ _ | _ \ _____ _|_ _|__ ___ | |___ @@ -88,14 +90,13 @@ public function __construct( } /** - * Gets the help message for the DevTools application, including the ASCII logo. + * Gets the help message for the DevTools application. * - * @return string */ #[Override] public function getHelp(): string { - return self::LOGO . "\n\n" . parent::getHelp(); + return parent::getHelp(); } /** @@ -128,6 +129,12 @@ protected function getDefaultInputDefinition(): InputDefinition description: 'Store generated DevTools artifacts in the given directory.', )); + $definition->addOption(new InputOption( + name: self::NO_LOGO_OPTION, + mode: InputOption::VALUE_NONE, + description: 'Hide the startup ASCII logo.', + )); + return $definition; } @@ -142,6 +149,10 @@ protected function getDefaultInputDefinition(): InputDefinition #[Override] public function doRun(InputInterface $input, OutputInterface $output): int { + if (! (bool) $input->getOption(self::NO_LOGO_OPTION)) { + $output->writeln(self::LOGO); + } + try { $this->workingDirectorySwitcher->switchTo($this->getWorkingDirectoryOption($input)); $this->configureWorkspaceDirectory($input); diff --git a/src/Process/ProcessBuilder.php b/src/Process/ProcessBuilder.php index 41e61e0da8..d76b0446d6 100644 --- a/src/Process/ProcessBuilder.php +++ b/src/Process/ProcessBuilder.php @@ -19,6 +19,7 @@ namespace FastForward\DevTools\Process; +use FastForward\DevTools\Path\DevToolsPathResolver; use Symfony\Component\Process\Process; /** @@ -31,6 +32,8 @@ */ final readonly class ProcessBuilder implements ProcessBuilderInterface { + private const string NO_LOGO_ARGUMENT = '--no-logo'; + /** * Creates a new immutable process builder instance. * @@ -94,10 +97,60 @@ public function getArguments(): array */ public function build(string|array $command): Process { + if (\is_array($command)) { + $command = array_values($command); + } + if (\is_string($command)) { $command = explode(' ', $command); } + if ($this->shouldAddLogoSuppressionArgument($command)) { + $command = $this->prependLogoSuppressionArgument($command); + } + return new Process(command: [...$command, ...$this->arguments], timeout: 0); } + + /** + * @param list $command + */ + private function shouldAddLogoSuppressionArgument(array $command): bool + { + if (\in_array(self::NO_LOGO_ARGUMENT, $this->arguments, true)) { + return false; + } + + if (0 === \count($command)) { + return false; + } + + $binary = \str_replace('\\', '/', $command[0]); + $packageBinaryPath = \str_replace('\\', '/', DevToolsPathResolver::getBinaryPath()); + + return $binary === $packageBinaryPath + || \str_starts_with($binary, 'vendor/bin/dev-tools') + || \str_starts_with($binary, './vendor/bin/dev-tools') + || \str_starts_with($binary, 'bin/dev-tools') + || \str_starts_with($binary, './bin/dev-tools') + || \str_ends_with($binary, '/vendor/bin/dev-tools') + || \str_ends_with($binary, '/vendor/fast-forward/dev-tools/bin/dev-tools') + || \str_ends_with($binary, '/bin/dev-tools'); + } + + /** + * @param list $command + * + * @return list + */ + private function prependLogoSuppressionArgument(array $command): array + { + if (0 === \count($command)) { + return $command; + } + + $binary = \array_shift($command); + + return [$binary, self::NO_LOGO_ARGUMENT, ...$command]; + } } diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index 15f453e5f4..ba71409bd3 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -62,8 +62,10 @@ use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\BufferedOutput; use function Safe\putenv; @@ -215,6 +217,58 @@ public function constructorWillRegisterGlobalRuntimeOptions(): void self::assertTrue($definition->hasOption('workspace-dir')); self::assertSame('w', $definition->getOption('workspace-dir')->getShortcut()); self::assertTrue($definition->hasOption('auto-update')); + self::assertTrue($definition->hasOption('no-logo')); + self::assertFalse($definition->getOption('no-logo')->acceptsValue()); + } + + /** + * @return void + */ + #[Test] + public function doRunWillRenderLogoUnlessNoLogoOptionIsProvided(): void + { + $input = new ArrayInput([ + 'command' => 'list', + ]); + + $output = new BufferedOutput(); + + $this->environment->get('FAST_FORWARD_AUTO_UPDATE', '') + ->willReturn(''); + $this->workingDirectorySwitcher->switchTo(null) + ->shouldBeCalledOnce(); + $this->versionCheckNotifier->notify($output) + ->shouldBeCalledOnce(); + + $result = $this->invokeDoRun($input, $output); + + self::assertSame(Command::SUCCESS, $result); + self::assertStringContainsString('_____', $output->fetch()); + } + + /** + * @return void + */ + #[Test] + public function doRunWillNotRenderLogoWhenNoLogoOptionIsSet(): void + { + $input = new ArrayInput([ + '--no-logo' => true, + 'command' => 'list', + ]); + + $output = new BufferedOutput(); + + $this->environment->get('FAST_FORWARD_AUTO_UPDATE', '') + ->willReturn(''); + $this->workingDirectorySwitcher->switchTo(null) + ->shouldBeCalledOnce(); + $this->versionCheckNotifier->notify($output) + ->shouldNotBeCalled(); + + $this->invokeDoRun($input, $output); + + self::assertStringNotContainsString('_____', $output->fetch()); } /** @@ -404,4 +458,17 @@ private function invokeConfigureWorkspaceDirectory(InputInterface $input): void $reflectionMethod = new ReflectionMethod($this->devTools, 'configureWorkspaceDirectory'); $reflectionMethod->invoke($this->devTools, $input); } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + private function invokeDoRun(InputInterface $input, OutputInterface $output): int + { + $reflectionMethod = new ReflectionMethod($this->devTools, 'doRun'); + + return (int) $reflectionMethod->invoke($this->devTools, $input, $output); + } } diff --git a/tests/Process/ProcessBuilderTest.php b/tests/Process/ProcessBuilderTest.php index 4721863c57..da5fdeda92 100644 --- a/tests/Process/ProcessBuilderTest.php +++ b/tests/Process/ProcessBuilderTest.php @@ -19,6 +19,7 @@ namespace FastForward\DevTools\Tests\Process; +use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilder; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -119,4 +120,48 @@ public function buildWillReturnProcessInstanceWithArguments(): void self::assertInstanceOf(Process::class, $process); self::assertSame("'php' 'artisan' 'serve' '--verbose' '--env=dev'", $process->getCommandLine()); } + + /** + * @return void + */ + #[Test] + public function buildWillInjectNoLogoArgumentForDevToolsCommands(): void + { + $process = $this->builder + ->build(DevToolsPathResolver::getBinaryCommand('tests')); + + self::assertSame( + "'" . DevToolsPathResolver::getBinaryPath() . "' '--no-logo' 'tests'", + $process->getCommandLine(), + ); + } + + /** + * @return void + */ + #[Test] + public function buildWillKeepExistingNoLogoArgumentWhenProvidedInArguments(): void + { + $process = $this->builder + ->withArgument('--no-logo') + ->withArgument('--ansi') + ->build(DevToolsPathResolver::getBinaryCommand('tests')); + + self::assertSame( + "'" . DevToolsPathResolver::getBinaryPath() . "' 'tests' '--no-logo' '--ansi'", + $process->getCommandLine(), + ); + } + + /** + * @return void + */ + #[Test] + public function buildWillNotInjectNoLogoArgumentForNonDevToolsCommands(): void + { + $process = $this->builder + ->build('vendor/bin/phpunit'); + + self::assertSame("'vendor/bin/phpunit'", $process->getCommandLine()); + } } From 870cd17a4a8b1b8efd5ed5cd80df1ba3fecf0418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:43:03 -0300 Subject: [PATCH 2/9] [tests] Fix no-logo option handling and assertions --- src/Console/DevTools.php | 22 +++++++--------------- src/Process/ProcessBuilder.php | 21 +++++++-------------- tests/Console/DevToolsTest.php | 2 +- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 234c88314d..91a44a3328 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -47,8 +47,6 @@ */ final class DevTools extends Application { - private const string NO_LOGO_OPTION = 'no-logo'; - private const string LOGO = <<<'LOGO' ____ _____ _ | _ \ _____ _|_ _|__ ___ | |___ @@ -57,6 +55,8 @@ final class DevTools extends Application |____/ \___| \_/ |_|\___/ \___/|_|___/ LOGO; + private const string NO_LOGO_OPTION = 'no-logo'; + /** * @var ContainerInterface holds the static container instance for global access within the DevTools context */ @@ -89,16 +89,6 @@ public function __construct( $this->setCommandLoader($commandLoader); } - /** - * Gets the help message for the DevTools application. - * - */ - #[Override] - public function getHelp(): string - { - return parent::getHelp(); - } - /** * Returns the application-level input definition with DevTools runtime options. * @@ -130,7 +120,7 @@ protected function getDefaultInputDefinition(): InputDefinition )); $definition->addOption(new InputOption( - name: self::NO_LOGO_OPTION, + name: 'no-logo', mode: InputOption::VALUE_NONE, description: 'Hide the startup ASCII logo.', )); @@ -149,7 +139,9 @@ protected function getDefaultInputDefinition(): InputDefinition #[Override] public function doRun(InputInterface $input, OutputInterface $output): int { - if (! (bool) $input->getOption(self::NO_LOGO_OPTION)) { + $noLogo = (bool) $input->getParameterOption('--no-logo', null, true); + + if (! $noLogo) { $output->writeln(self::LOGO); } @@ -162,7 +154,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - if (! $this->isSelfUpdateCommand($input)) { + if (! $noLogo && ! $this->isSelfUpdateCommand($input)) { $this->runAutoUpdateWhenRequested($input, $output); $this->versionCheckNotifier->notify($output); } diff --git a/src/Process/ProcessBuilder.php b/src/Process/ProcessBuilder.php index d76b0446d6..87f8283588 100644 --- a/src/Process/ProcessBuilder.php +++ b/src/Process/ProcessBuilder.php @@ -121,21 +121,14 @@ private function shouldAddLogoSuppressionArgument(array $command): bool return false; } - if (0 === \count($command)) { + if ([] === $command) { return false; } - $binary = \str_replace('\\', '/', $command[0]); - $packageBinaryPath = \str_replace('\\', '/', DevToolsPathResolver::getBinaryPath()); - - return $binary === $packageBinaryPath - || \str_starts_with($binary, 'vendor/bin/dev-tools') - || \str_starts_with($binary, './vendor/bin/dev-tools') - || \str_starts_with($binary, 'bin/dev-tools') - || \str_starts_with($binary, './bin/dev-tools') - || \str_ends_with($binary, '/vendor/bin/dev-tools') - || \str_ends_with($binary, '/vendor/fast-forward/dev-tools/bin/dev-tools') - || \str_ends_with($binary, '/bin/dev-tools'); + $binary = str_replace('\\', '/', $command[0]); + $packageBinaryPath = str_replace('\\', '/', DevToolsPathResolver::getBinaryPath()); + + return $binary === $packageBinaryPath; } /** @@ -145,11 +138,11 @@ private function shouldAddLogoSuppressionArgument(array $command): bool */ private function prependLogoSuppressionArgument(array $command): array { - if (0 === \count($command)) { + if ([] === $command) { return $command; } - $binary = \array_shift($command); + $binary = array_shift($command); return [$binary, self::NO_LOGO_ARGUMENT, ...$command]; } diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index ba71409bd3..55f6060216 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -218,7 +218,7 @@ public function constructorWillRegisterGlobalRuntimeOptions(): void self::assertSame('w', $definition->getOption('workspace-dir')->getShortcut()); self::assertTrue($definition->hasOption('auto-update')); self::assertTrue($definition->hasOption('no-logo')); - self::assertFalse($definition->getOption('no-logo')->acceptsValue()); + self::assertFalse($definition->getOption('no-logo')->acceptValue()); } /** From d088f2bd19dac2cdf810cf2da8c36ce51e00318b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:46:20 +0000 Subject: [PATCH 3/9] Update wiki submodule pointer for PR #278 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index f1a8bc31ab..4782e85916 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit f1a8bc31ab0d8e09146703773fceb77157c5d2ba +Subproject commit 4782e85916e367116b8278df8be71c784a2785f5 From 027118a533e6c9ac933c5a0e4707621e58424b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:47:08 -0300 Subject: [PATCH 4/9] Refactor logo display in DevTools class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/DevTools.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 91a44a3328..89b5ef320f 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -53,9 +53,9 @@ final class DevTools extends Application | | | |/ _ \ \ / / | |/ _ \ / _ \| / __| | |_| | __/\ V / | | (_) | (_) | \__ \ |____/ \___| \_/ |_|\___/ \___/|_|___/ - LOGO; + ======================================== - private const string NO_LOGO_OPTION = 'no-logo'; + LOGO; /** * @var ContainerInterface holds the static container instance for global access within the DevTools context From 1390eebecb20853a29aa3e2e56e380397c3a0b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:49:40 -0300 Subject: [PATCH 5/9] fix: add coverage metadata for logo banner behavior --- CHANGELOG.md | 2 +- tests/Console/Command/DependenciesCommandTest.php | 2 ++ tests/Process/ProcessBuilderTest.php | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0555a73e..eb2f289138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Hide DevTools logo in nested subprocesses by adding a new `--no-logo` global option for the top-level application and automatically attaching it to internal DevTools child processes, avoiding banner repetition in orchestrated command queues (#277) +- Show the DevTools ASCII logo by default on all top-level command executions, while adding a `--no-logo` global option that is automatically forwarded to internal DevTools subprocesses to avoid banner repetition in orchestrated command queues (#277) ### Added diff --git a/tests/Console/Command/DependenciesCommandTest.php b/tests/Console/Command/DependenciesCommandTest.php index 42c26e184c..aac20c26d7 100644 --- a/tests/Console/Command/DependenciesCommandTest.php +++ b/tests/Console/Command/DependenciesCommandTest.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Console\Command\DependenciesCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig; +use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilder; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; @@ -42,6 +43,7 @@ use Symfony\Component\Process\Process; #[CoversClass(DependenciesCommand::class)] +#[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ProcessBuilder::class)] #[UsesTrait(LogsCommandResults::class)] final class DependenciesCommandTest extends TestCase diff --git a/tests/Process/ProcessBuilderTest.php b/tests/Process/ProcessBuilderTest.php index da5fdeda92..e5077b3410 100644 --- a/tests/Process/ProcessBuilderTest.php +++ b/tests/Process/ProcessBuilderTest.php @@ -22,12 +22,14 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilder; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Process\Process; #[CoversClass(ProcessBuilder::class)] +#[UsesClass(DevToolsPathResolver::class)] final class ProcessBuilderTest extends TestCase { use ProphecyTrait; From 2cebc678e44896ca27e276d186754b4a3e7a3dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:51:36 -0300 Subject: [PATCH 6/9] fix: hide logo for JSON output modes --- CHANGELOG.md | 2 +- src/Console/DevTools.php | 4 +- tests/Console/DevToolsTest.php | 91 ++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb2f289138..b6b1808120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Show the DevTools ASCII logo by default on all top-level command executions, while adding a `--no-logo` global option that is automatically forwarded to internal DevTools subprocesses to avoid banner repetition in orchestrated command queues (#277) +- Show the DevTools ASCII logo by default on all top-level command executions, while adding a `--no-logo` global option and automatically suppressing the banner for `--json` / `--pretty-json` invocations (including automatic forwarding of `--no-logo` to internal DevTools subprocesses) to avoid banner repetition in orchestrated command queues (#277) ### Added diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 89b5ef320f..607e102651 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -139,7 +139,9 @@ protected function getDefaultInputDefinition(): InputDefinition #[Override] public function doRun(InputInterface $input, OutputInterface $output): int { - $noLogo = (bool) $input->getParameterOption('--no-logo', null, true); + $noLogo = (bool) $input->getParameterOption('--no-logo', null, true) + || (bool) $input->hasParameterOption('--json', true) + || (bool) $input->hasParameterOption('--pretty-json', true); if (! $noLogo) { $output->writeln(self::LOGO); diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index 55f6060216..e649b9e9d1 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -63,6 +63,7 @@ use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\BufferedOutput; @@ -271,6 +272,96 @@ public function doRunWillNotRenderLogoWhenNoLogoOptionIsSet(): void self::assertStringNotContainsString('_____', $output->fetch()); } + /** + * @return void + */ + #[Test] + public function doRunWillNotRenderLogoWhenJsonOptionIsProvided(): void + { + $command = new class extends Command { + public function __construct() + { + parent::__construct('standards'); + } + + protected function configure(): void + { + $this->addOption(name: 'json', mode: InputOption::VALUE_NONE, description: 'Emit structured JSON output.'); + $this->setCode(static fn(InputInterface $input, OutputInterface $output): int => Command::SUCCESS); + } + }; + + $this->commandLoader->has('standards') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->get('standards') + ->willReturn($command) + ->shouldBeCalledOnce(); + $input = new ArrayInput([ + 'command' => 'standards', + '--json' => true, + ]); + + $output = new BufferedOutput(); + + $this->environment->get('FAST_FORWARD_AUTO_UPDATE', '') + ->willReturn(''); + $this->workingDirectorySwitcher->switchTo(null) + ->shouldBeCalledOnce(); + $this->versionCheckNotifier->notify($output) + ->shouldNotBeCalled(); + + $result = $this->invokeDoRun($input, $output); + + self::assertSame(Command::SUCCESS, $result); + self::assertStringNotContainsString('_____', $output->fetch()); + } + + /** + * @return void + */ + #[Test] + public function doRunWillNotRenderLogoWhenPrettyJsonOptionIsProvided(): void + { + $command = new class extends Command { + public function __construct() + { + parent::__construct('standards'); + } + + protected function configure(): void + { + $this->addOption(name: 'pretty-json', mode: InputOption::VALUE_NONE, description: 'Emit pretty JSON output.'); + $this->setCode(static fn(InputInterface $input, OutputInterface $output): int => Command::SUCCESS); + } + }; + + $this->commandLoader->has('standards') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->get('standards') + ->willReturn($command) + ->shouldBeCalledOnce(); + $input = new ArrayInput([ + 'command' => 'standards', + '--pretty-json' => true, + ]); + + $output = new BufferedOutput(); + + $this->environment->get('FAST_FORWARD_AUTO_UPDATE', '') + ->willReturn(''); + $this->workingDirectorySwitcher->switchTo(null) + ->shouldBeCalledOnce(); + $this->versionCheckNotifier->notify($output) + ->shouldNotBeCalled(); + + $result = $this->invokeDoRun($input, $output); + + self::assertSame(Command::SUCCESS, $result); + self::assertStringNotContainsString('_____', $output->fetch()); + } + /** * @return void */ From eb6008c080e6b87700b29d5ce0bf32f03935e159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:53:45 -0300 Subject: [PATCH 7/9] test: add coverage metadata for tests command --- tests/Console/Command/TestsCommandTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 0c6b43548d..3ed20e666f 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -28,6 +28,7 @@ use FastForward\DevTools\Process\ProcessBuilder; 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; @@ -48,6 +49,7 @@ #[CoversClass(TestsCommand::class)] #[UsesClass(CoverageSummary::class)] +#[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ProcessBuilder::class)] #[UsesClass(ManagedWorkspace::class)] #[UsesTrait(LogsCommandResults::class)] From d7242cfe63f70a2bb6f40d4b0e686ea49eea6523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 19:01:36 -0300 Subject: [PATCH 8/9] fix: suppress logo for raw changelog commands --- src/Console/DevTools.php | 14 +++++++++++ tests/Console/DevToolsTest.php | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 607e102651..cf2bb5113c 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -142,6 +142,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int $noLogo = (bool) $input->getParameterOption('--no-logo', null, true) || (bool) $input->hasParameterOption('--json', true) || (bool) $input->hasParameterOption('--pretty-json', true); + $noLogo = $noLogo || $this->isRawOutputCommand($input); if (! $noLogo) { $output->writeln(self::LOGO); @@ -253,6 +254,19 @@ private function isSelfUpdateCommand(InputInterface $input): bool return \in_array($input->getFirstArgument(), SelfUpdateCommand::getCommandNames(), true); } + /** + * Identifies commands that must keep CLI output unprefixed by logos. + * + * @param InputInterface $input + */ + private function isRawOutputCommand(InputInterface $input): bool + { + return \in_array((string) $input->getFirstArgument(), [ + 'changelog:next-version', + 'changelog:show', + ], true); + } + /** * Interprets environment values that enable auto-update. * diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index e649b9e9d1..d1767bfff7 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -362,6 +362,52 @@ protected function configure(): void self::assertStringNotContainsString('_____', $output->fetch()); } + /** + * @return void + */ + #[Test] + public function doRunWillNotRenderLogoForRawChangelogCommands(): void + { + foreach (['changelog:next-version', 'changelog:show'] as $commandName) { + $this->commandLoader->has($commandName) + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->get($commandName) + ->willReturn( + new class($commandName) extends Command { + public function __construct(string $name) + { + parent::__construct($name); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + return Command::SUCCESS; + } + }, + ) + ->shouldBeCalledOnce(); + + $input = new ArrayInput([ + 'command' => $commandName, + ]); + + $output = new BufferedOutput(); + + $this->environment->get('FAST_FORWARD_AUTO_UPDATE', '') + ->willReturn(''); + $this->workingDirectorySwitcher->switchTo(null) + ->shouldBeCalledOnce(); + $this->versionCheckNotifier->notify($output) + ->shouldNotBeCalled(); + + $result = $this->invokeDoRun($input, $output); + + self::assertSame(Command::SUCCESS, $result); + self::assertStringNotContainsString('_____', $output->fetch()); + } + } + /** * @return void */ From a4421a60dc3bf7b8ee5c085016140c3a43fc4292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 19:03:52 -0300 Subject: [PATCH 9/9] refactor: centralize logo rendering decision --- src/Console/DevTools.php | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index cf2bb5113c..2c09e1a7b4 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -47,6 +47,11 @@ */ final class DevTools extends Application { + private const array RAW_OUTPUT_COMMANDS = [ + 'changelog:next-version', + 'changelog:show', + ]; + private const string LOGO = <<<'LOGO' ____ _____ _ | _ \ _____ _|_ _|__ ___ | |___ @@ -139,12 +144,9 @@ protected function getDefaultInputDefinition(): InputDefinition #[Override] public function doRun(InputInterface $input, OutputInterface $output): int { - $noLogo = (bool) $input->getParameterOption('--no-logo', null, true) - || (bool) $input->hasParameterOption('--json', true) - || (bool) $input->hasParameterOption('--pretty-json', true); - $noLogo = $noLogo || $this->isRawOutputCommand($input); + $shouldRenderLogo = $this->shouldRenderLogo($input); - if (! $noLogo) { + if ($shouldRenderLogo) { $output->writeln(self::LOGO); } @@ -157,7 +159,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - if (! $noLogo && ! $this->isSelfUpdateCommand($input)) { + if ($shouldRenderLogo && ! $this->isSelfUpdateCommand($input)) { $this->runAutoUpdateWhenRequested($input, $output); $this->versionCheckNotifier->notify($output); } @@ -254,6 +256,29 @@ private function isSelfUpdateCommand(InputInterface $input): bool return \in_array($input->getFirstArgument(), SelfUpdateCommand::getCommandNames(), true); } + /** + * Whether to show the startup ASCII logo for this invocation. + * + * @param InputInterface $input + */ + private function shouldRenderLogo(InputInterface $input): bool + { + return ! $this->isLogoSuppressedByOptions($input) + && ! $this->isRawOutputCommand($input); + } + + /** + * Detects CLI flags that explicitly suppress logo output. + * + * @param InputInterface $input + */ + private function isLogoSuppressedByOptions(InputInterface $input): bool + { + return (bool) $input->getParameterOption('--no-logo', null, true) + || (bool) $input->hasParameterOption('--json', true) + || (bool) $input->hasParameterOption('--pretty-json', true); + } + /** * Identifies commands that must keep CLI output unprefixed by logos. * @@ -261,10 +286,7 @@ private function isSelfUpdateCommand(InputInterface $input): bool */ private function isRawOutputCommand(InputInterface $input): bool { - return \in_array((string) $input->getFirstArgument(), [ - 'changelog:next-version', - 'changelog:show', - ], true); + return \in_array((string) $input->getFirstArgument(), self::RAW_OUTPUT_COMMANDS, true); } /**