From e2d4928c1cf486e9dce0c5c128f1a93e9c1e0bec Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 22 Jul 2026 20:33:19 +0100 Subject: [PATCH 1/3] feat: Refactor success message display and enhance next steps output in InstallCommand --- src/Console/Commands/InstallCommand.php | 24 ++++++++------- src/Environments/DockerEnvironment.php | 6 ++-- src/Environments/Environment.php | 14 ++++++++- src/Environments/NativeEnvironment.php | 6 ++-- tests/Feature/InstallCommandTest.php | 39 ++++++++++--------------- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php index 033e0f8..7501805 100644 --- a/src/Console/Commands/InstallCommand.php +++ b/src/Console/Commands/InstallCommand.php @@ -4,11 +4,13 @@ use Illuminate\Console\Command; use Illuminate\Support\Str; +use Laravel\Prompts\Elements\Element; use Saucebase\Installer\Console\Commands\Concerns\DisplaysBanner; use Saucebase\Installer\Environments\Environment; use Saucebase\Installer\ModuleRegistry; use Symfony\Component\Process\Process; +use function Laravel\Prompts\callout; use function Laravel\Prompts\select; class InstallCommand extends Command @@ -515,16 +517,16 @@ protected function isCI(): bool public function displaySuccess(array $steps = []): void { - $this->newLine(); - $this->info('Installation complete!'); - $this->newLine(); - if ($steps) { - $this->line('Next steps:'); - foreach (array_values($steps) as $i => $step) { - $this->line(' '.($i + 1).'. '.$step); - } - $this->newLine(); - } - $this->line('Learn more: https://github.com/saucebase-dev/saucebase'); + callout(label: 'Installation complete', content: $this->successCalloutContent($steps)); + } + + /** @return array */ + protected function successCalloutContent(array $steps): array + { + return array_filter([ + $steps ? 'You can start your local development using:' : null, + $steps ? Element::numberedList(array_values($steps)) : null, + 'Learn more: '.Element::link('https://github.com/saucebase-dev/saucebase'), + ]); } } diff --git a/src/Environments/DockerEnvironment.php b/src/Environments/DockerEnvironment.php index 39c16db..b1acfe6 100644 --- a/src/Environments/DockerEnvironment.php +++ b/src/Environments/DockerEnvironment.php @@ -301,9 +301,9 @@ protected function nextSteps(InstallCommand $command): array $appUrl = $this->readEnvValue($command, 'APP_URL') ?? ($this->ssl ? 'https://localhost' : 'http://localhost'); return [ - 'Compile frontend assets: npm install && npm run dev', - 'Open your app: '.$appUrl.'', - 'Email testing (Mailpit): http://localhost:8025', + 'Compile frontend assets: `npm install && npm run dev`', + 'Open your app: `'.$appUrl.'`', + 'Email testing (Mailpit): `http://localhost:8025`', ]; } diff --git a/src/Environments/Environment.php b/src/Environments/Environment.php index 1e9e627..7c352c3 100644 --- a/src/Environments/Environment.php +++ b/src/Environments/Environment.php @@ -33,12 +33,24 @@ public function run(InstallCommand $command): int $result = $this->boot($command); if ($result === InstallCommand::SUCCESS) { - $command->displaySuccess($this->nextSteps($command)); + $command->displaySuccess(array_merge($this->cdStep($command), $this->nextSteps($command))); } return $result; } + /** @return string[] A `cd` step when the target app lives outside the current directory, empty otherwise. */ + protected function cdStep(InstallCommand $command): array + { + $target = rtrim($command->targetPath(), '/'); + + if ($target === rtrim(getcwd(), '/')) { + return []; + } + + return ['cd `'.basename($target).'`']; + } + /** Hook: perform driver-specific steps before the module prompt. Return an exit code to abort, null to continue. */ protected function beforePrompts(InstallCommand $command): ?int { diff --git a/src/Environments/NativeEnvironment.php b/src/Environments/NativeEnvironment.php index 6dcb3ab..3f942a7 100644 --- a/src/Environments/NativeEnvironment.php +++ b/src/Environments/NativeEnvironment.php @@ -35,9 +35,9 @@ protected function boot(InstallCommand $command): int protected function nextSteps(InstallCommand $command): array { return [ - 'Install frontend dependencies: npm install', - 'Start the dev server: composer dev', - 'Open your app: '.($this->readEnvValue($command, 'APP_URL') ?? 'http://localhost').'', + 'Install frontend dependencies: `npm install`', + 'Start the dev server: `composer dev`', + 'Open your app: `'.($this->readEnvValue($command, 'APP_URL') ?? 'http://localhost').'`', ]; } } diff --git a/tests/Feature/InstallCommandTest.php b/tests/Feature/InstallCommandTest.php index be82522..49374f0 100644 --- a/tests/Feature/InstallCommandTest.php +++ b/tests/Feature/InstallCommandTest.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; +use Laravel\Prompts\Elements\NumberedList; use Saucebase\Installer\Console\Commands\InstallCommand; use Saucebase\Installer\Environments\Environment; use Saucebase\Installer\Environments\NativeEnvironment; @@ -611,34 +612,19 @@ public function test_module_has_seeder_checks_vendor_path(): void public function test_display_success_numbers_steps_sequentially(): void { - $cmd = new class extends TestableInstallCommand - { - public array $capturedLines = []; - - public function line($string, $style = null, $verbosity = null): void - { - $this->capturedLines[] = $string; - } + $cmd = new TestableInstallCommand; - public function info($string, $verbosity = null): void {} + $content = $cmd->exposedSuccessCalloutContent([5 => 'first step', 10 => 'second step']); - public function newLine($count = 1): static - { - return $this; + $list = null; + foreach ($content as $part) { + if ($part instanceof NumberedList) { + $list = $part; } - }; - - $cmd->displaySuccess([5 => 'first step', 10 => 'second step']); - - $stepLines = implode(' ', array_filter( - $cmd->capturedLines, - fn ($l) => (bool) preg_match('/\d+\./', $l), - )); + } - $this->assertStringContainsString('1. first step', $stepLines); - $this->assertStringContainsString('2. second step', $stepLines); - $this->assertStringNotContainsString('6. first step', $stepLines); - $this->assertStringNotContainsString('11. second step', $stepLines); + $this->assertNotNull($list); + $this->assertSame(['first step', 'second step'], $list->items); } // ------------------------------------------------------------------------- @@ -751,6 +737,11 @@ public function exposedFilterModulesByFramework(array $packages, string $framewo return $this->filterModulesByFramework($packages, $framework); } + public function exposedSuccessCalloutContent(array $steps): array + { + return $this->successCalloutContent($steps); + } + /** @var array Fake option values for tests that bypass CLI input. */ public array $fakeOptions = []; From 58224da59f2f3c8da9d33fa4eeb84a9ec0809151 Mon Sep 17 00:00:00 2001 From: roble <3231587+roble@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:34:35 +0000 Subject: [PATCH 2/3] PHP Linting (Pint) --- src/Console/Commands/InstallCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php index 7501805..ca2e48b 100644 --- a/src/Console/Commands/InstallCommand.php +++ b/src/Console/Commands/InstallCommand.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Str; use Laravel\Prompts\Elements\Element; +use Laravel\Prompts\Elements\ElementContract; use Saucebase\Installer\Console\Commands\Concerns\DisplaysBanner; use Saucebase\Installer\Environments\Environment; use Saucebase\Installer\ModuleRegistry; @@ -520,7 +521,7 @@ public function displaySuccess(array $steps = []): void callout(label: 'Installation complete', content: $this->successCalloutContent($steps)); } - /** @return array */ + /** @return array */ protected function successCalloutContent(array $steps): array { return array_filter([ From be78c111dfdef0d79bc99a5a918069a2abebea76 Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 22 Jul 2026 20:55:52 +0100 Subject: [PATCH 3/3] feat: Enhance cdStep method to normalize paths and add tests for various target scenarios --- src/Environments/Environment.php | 14 ++- .../Environments/NativeEnvironmentTest.php | 105 ++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/Environments/Environment.php b/src/Environments/Environment.php index 7c352c3..ac2a3c4 100644 --- a/src/Environments/Environment.php +++ b/src/Environments/Environment.php @@ -42,13 +42,21 @@ public function run(InstallCommand $command): int /** @return string[] A `cd` step when the target app lives outside the current directory, empty otherwise. */ protected function cdStep(InstallCommand $command): array { - $target = rtrim($command->targetPath(), '/'); + $target = $this->normalizePath($command->targetPath()); + $cwd = $this->normalizePath(getcwd()); - if ($target === rtrim(getcwd(), '/')) { + if ($target === $cwd) { return []; } - return ['cd `'.basename($target).'`']; + return ['cd `'.$target.'`']; + } + + private function normalizePath(string $path): string + { + $real = realpath($path); + + return rtrim($real !== false ? $real : $path, '/'); } /** Hook: perform driver-specific steps before the module prompt. Return an exit code to abort, null to continue. */ diff --git a/tests/Feature/Environments/NativeEnvironmentTest.php b/tests/Feature/Environments/NativeEnvironmentTest.php index 65f5e98..02afb81 100644 --- a/tests/Feature/Environments/NativeEnvironmentTest.php +++ b/tests/Feature/Environments/NativeEnvironmentTest.php @@ -106,4 +106,109 @@ public function install(): int $this->assertSame(Command::FAILURE, $env->run($command)); } + + // ------------------------------------------------------------------------- + // cdStep — inherited from Environment + // ------------------------------------------------------------------------- + + private string $originalCwd; + + private string $sandbox; + + protected function setUp(): void + { + parent::setUp(); + $this->originalCwd = getcwd(); + $this->sandbox = sys_get_temp_dir().'/saucebase-env-test-'.uniqid(); + mkdir($this->sandbox.'/sub/dir', recursive: true); + chdir($this->sandbox); + } + + protected function tearDown(): void + { + chdir($this->originalCwd); + $this->removeDirectory($this->sandbox); + parent::tearDown(); + } + + private function envExposingCdStep(): object + { + return new class extends NativeEnvironment + { + public function exposedCdStep(InstallCommand $command): array + { + return $this->cdStep($command); + } + }; + } + + public function test_cd_step_is_empty_when_target_matches_cwd(): void + { + $command = new class extends InstallCommand + { + public function targetPath(): string + { + return getcwd(); + } + }; + + $this->assertSame([], $this->envExposingCdStep()->exposedCdStep($command)); + } + + public function test_cd_step_resolves_nested_relative_target(): void + { + $command = new class extends InstallCommand + { + public function targetPath(): string + { + return './sub/../sub/dir'; + } + }; + + $this->assertSame( + ['cd `'.realpath($this->sandbox.'/sub/dir').'`'], + $this->envExposingCdStep()->exposedCdStep($command), + ); + } + + public function test_cd_step_resolves_absolute_target_outside_cwd(): void + { + $elsewhere = sys_get_temp_dir().'/saucebase-env-test-elsewhere-'.uniqid(); + mkdir($elsewhere, recursive: true); + + $command = new class($elsewhere) extends InstallCommand + { + public function __construct(private string $elsewhere) {} + + public function targetPath(): string + { + return $this->elsewhere; + } + }; + + $this->assertSame( + ['cd `'.realpath($elsewhere).'`'], + $this->envExposingCdStep()->exposedCdStep($command), + ); + + $this->removeDirectory($elsewhere); + } + + private function removeDirectory(string $dir): void + { + if (! is_dir($dir)) { + return; + } + + foreach (scandir($dir) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $path = $dir.'/'.$entry; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + + rmdir($dir); + } }