From 942cb807558c24cd35f7ccc277a4d34157a5ed7e Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Sun, 27 Sep 2026 10:54:02 +0200 Subject: [PATCH 1/2] Show colors in the output of `bref command` When the output is a terminal, `bref command` asks Bref Cloud for colors: Bref Cloud adds `--ansi` to Laravel and Symfony commands, which otherwise never write colors on Lambda (their output is not a terminal there). The output is printed as it was, the terminal renders the colors. When the output is piped or redirected, nothing changes. Claude-Session: https://claude.ai/code/session_01Awk33iZ9mDSg1xgSAPkgGK --- src/BrefCloudClient.php | 4 +- src/Commands/Command.php | 11 +++++- tests/Commands/CommandTest.php | 60 ++++++++++++++++++++++++++++++ tests/Commands/CommandTestCase.php | 7 +++- 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/Commands/CommandTest.php diff --git a/src/BrefCloudClient.php b/src/BrefCloudClient.php index 6ac0c6e..7fab979 100644 --- a/src/BrefCloudClient.php +++ b/src/BrefCloudClient.php @@ -305,15 +305,17 @@ public function removeEnvironment(int $environmentId): void } /** + * @param bool $ansi Ask for colors: Bref Cloud adds `--ansi` to the command when the application's console supports it. * @throws HttpExceptionInterface * @throws ExceptionInterface */ - public function startCommand(int $environmentId, string $command): int + public function startCommand(int $environmentId, string $command, bool $ansi = false): int { return $this->client->request('POST', '/api/v1/commands/start', [ 'json' => [ 'environmentId' => $environmentId, 'command' => $command, + 'ansi' => $ansi, ], ])->toArray()['id']; } diff --git a/src/Commands/Command.php b/src/Commands/Command.php index 0b0a29e..8affa2c 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -13,6 +13,12 @@ class Command extends ApplicationCommand { + public function __construct( + private readonly ?BrefCloudClient $brefCloud = null, + ) { + parent::__construct(); + } + protected function configure(): void { $this @@ -35,9 +41,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int IO::spin('starting command'); - $brefCloud = new BrefCloudClient; + $brefCloud = $this->brefCloud ?? new BrefCloudClient; $environment = $brefCloud->findEnvironment($team, $appName, $environmentName); - $id = $brefCloud->startCommand($environment['id'], $command); + // Colors when the output is a terminal that shows them, the command's output is printed as is + $id = $brefCloud->startCommand($environment['id'], $command, $output->isDecorated()); IO::spin('running'); diff --git a/tests/Commands/CommandTest.php b/tests/Commands/CommandTest.php new file mode 100644 index 0000000..5ab4a4a --- /dev/null +++ b/tests/Commands/CommandTest.php @@ -0,0 +1,60 @@ +configFile = sys_get_temp_dir() . '/bref-cli-test-' . bin2hex(random_bytes(4)) . '.yml'; + file_put_contents($this->configFile, "service: shop\nbref:\n team: acme\n"); + } + + protected function tearDown(): void + { + unlink($this->configFile); + parent::tearDown(); + } + + public function test_asks_for_colors_when_the_output_is_a_terminal(): void + { + $tester = $this->runCommand(decorated: true); + + $this->assertSame(0, $tester->getStatusCode(), $tester->getDisplay()); + $this->assertSame(['environmentId' => 12, 'command' => 'about', 'ansi' => true], $this->requestBodies['/api/v1/commands/start']); + // Printed as is: the terminal renders the colors + $this->assertStringContainsString("\e[32mLaravel\e[39m", $tester->getDisplay()); + } + + public function test_no_colors_when_the_output_is_not_a_terminal(): void + { + $tester = $this->runCommand(decorated: false); + + $this->assertSame(0, $tester->getStatusCode(), $tester->getDisplay()); + $this->assertFalse($this->requestBodies['/api/v1/commands/start']['ansi']); + } + + private function runCommand(bool $decorated): ApplicationTester + { + // Through the application, which sets up the output of the `IO` class + $application = new Application; + restore_error_handler(); + $application->setAutoExit(false); + $application->safeAddCommand(new Command($this->brefCloud([ + '/api/v1/environments/find' => $this->environment(), + '/api/v1/commands/start' => ['id' => 5], + '/api/v1/commands/5' => ['status' => 'success', 'output' => "\e[32mLaravel\e[39m 13"], + ]))); + $tester = new ApplicationTester($application); + $tester->run(['command' => 'command', 'args' => 'about', '--config' => $this->configFile], ['decorated' => $decorated]); + + return $tester; + } +} diff --git a/tests/Commands/CommandTestCase.php b/tests/Commands/CommandTestCase.php index 3664714..d4819a0 100644 --- a/tests/Commands/CommandTestCase.php +++ b/tests/Commands/CommandTestCase.php @@ -14,6 +14,8 @@ abstract class CommandTestCase extends TestCase private array $agentVariables = []; /** @var list Paths and query strings of the requests sent to Bref Cloud */ protected array $requests = []; + /** @var array JSON bodies of the requests sent to Bref Cloud, indexed by path */ + protected array $requestBodies = []; protected function setUp(): void { @@ -41,10 +43,13 @@ protected function runByAnAgent(): void */ protected function brefCloud(array $routes): BrefCloudClient { - $client = new MockHttpClient(function (string $method, string $url) use ($routes): MockResponse { + $client = new MockHttpClient(function (string $method, string $url, array $options) use ($routes): MockResponse { $path = (string) parse_url($url, PHP_URL_PATH); $query = (string) parse_url($url, PHP_URL_QUERY); $this->requests[] = urldecode($path . ($query ? "?$query" : '')); + if (is_string($options['body'] ?? null) && $options['body'] !== '') { + $this->requestBodies[$path] = json_decode($options['body'], true); + } if (! array_key_exists($path, $routes)) { $this->fail("Unexpected request: $method $url"); } From ecc3ae2834c455ef23f8197c87f16ad704ce0d04 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Sun, 27 Sep 2026 21:05:39 +0200 Subject: [PATCH 2/2] Run the command test without the whole application Creating `Bref\Cli\Application` turns warnings into exceptions, and with the lowest dependencies (Symfony Console 5 on PHP 8.2) loading Symfony's `HelperSet` triggers a deprecation notice that then became a fatal error. The test now sets up `IO` like the application does, and runs the command directly. Claude-Session: https://claude.ai/code/session_01Awk33iZ9mDSg1xgSAPkgGK --- tests/Commands/CommandTest.php | 40 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/Commands/CommandTest.php b/tests/Commands/CommandTest.php index 5ab4a4a..18ee731 100644 --- a/tests/Commands/CommandTest.php +++ b/tests/Commands/CommandTest.php @@ -2,9 +2,10 @@ namespace Bref\Cli\Test\Commands; -use Bref\Cli\Application; +use Bref\Cli\Cli\IO; use Bref\Cli\Commands\Command; -use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; class CommandTest extends CommandTestCase { @@ -25,36 +26,41 @@ protected function tearDown(): void public function test_asks_for_colors_when_the_output_is_a_terminal(): void { - $tester = $this->runCommand(decorated: true); + [$status, $display] = $this->runCommand(decorated: true); - $this->assertSame(0, $tester->getStatusCode(), $tester->getDisplay()); + $this->assertSame(0, $status, $display); $this->assertSame(['environmentId' => 12, 'command' => 'about', 'ansi' => true], $this->requestBodies['/api/v1/commands/start']); // Printed as is: the terminal renders the colors - $this->assertStringContainsString("\e[32mLaravel\e[39m", $tester->getDisplay()); + $this->assertStringContainsString("\e[32mLaravel\e[39m", $display); } public function test_no_colors_when_the_output_is_not_a_terminal(): void { - $tester = $this->runCommand(decorated: false); + [$status, $display] = $this->runCommand(decorated: false); - $this->assertSame(0, $tester->getStatusCode(), $tester->getDisplay()); + $this->assertSame(0, $status, $display); $this->assertFalse($this->requestBodies['/api/v1/commands/start']['ansi']); } - private function runCommand(bool $decorated): ApplicationTester + /** + * @return array{int, string} The exit code and the output + */ + private function runCommand(bool $decorated): array { - // Through the application, which sets up the output of the `IO` class - $application = new Application; - restore_error_handler(); - $application->setAutoExit(false); - $application->safeAddCommand(new Command($this->brefCloud([ + $command = new Command($this->brefCloud([ '/api/v1/environments/find' => $this->environment(), '/api/v1/commands/start' => ['id' => 5], '/api/v1/commands/5' => ['status' => 'success', 'output' => "\e[32mLaravel\e[39m 13"], - ]))); - $tester = new ApplicationTester($application); - $tester->run(['command' => 'command', 'args' => 'about', '--config' => $this->configFile], ['decorated' => $decorated]); + ])); + $input = new ArrayInput(['args' => 'about', '--config' => $this->configFile]); + $input->setInteractive(false); + $output = new BufferedOutput(decorated: $decorated); + // What the application does before running a command + IO::init($input, $output); - return $tester; + $status = $command->run($input, $output); + IO::stop(); + + return [$status, $output->fetch()]; } }