From 73b3dc6590793f8a0e7ec0b62bd8c57cd913f41c Mon Sep 17 00:00:00 2001 From: roble Date: Fri, 19 Jun 2026 20:53:58 +0100 Subject: [PATCH 1/6] feat: add --no-logo option to suppress welcome banner in saucebase:install command --- README.md | 1 + src/Console/Commands/InstallCommand.php | 8 ++++++-- src/Environments/DockerEnvironment.php | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6786e3..b90e16b 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Prompts for the frontend stack (Vue or React) and the environment driver (Docker | `--modules=auth,billing` | Install specific modules by name (comma-separated) | | `--dev` | Contributor mode — skips module installation | | `--force` | Skip confirmations | +| `--no-logo` | Suppress the welcome banner (passed automatically when running inside a container) | **Examples** diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php index 67ac5a3..aed48c5 100644 --- a/src/Console/Commands/InstallCommand.php +++ b/src/Console/Commands/InstallCommand.php @@ -23,7 +23,8 @@ class InstallCommand extends Command {--all-modules : Enable and migrate all available modules without prompting} {--modules= : Comma-separated list of modules to enable (e.g. Auth,Settings)} {--dev : Dev environment} - {--force : Skip confirmations}'; + {--force : Skip confirmations} + {--no-logo : Suppress the welcome banner}'; protected $description = 'Install and configure Saucebase'; @@ -40,7 +41,10 @@ class InstallCommand extends Command public function handle(): int { - $this->displayWelcome(); + if (! $this->option('no-logo')) { + $this->displayWelcome(); + } + $this->captureStack(); if ($this->isCI()) { diff --git a/src/Environments/DockerEnvironment.php b/src/Environments/DockerEnvironment.php index 1ab0c21..9cae4b6 100644 --- a/src/Environments/DockerEnvironment.php +++ b/src/Environments/DockerEnvironment.php @@ -154,7 +154,7 @@ protected function runNpmOnHost(InstallCommand $command): void /** @return string[] */ public function buildContainerArgs(InstallCommand $command): array { - $args = ['php', 'artisan', 'saucebase:install', '--driver=native', '--force']; + $args = ['php', 'artisan', 'saucebase:install', '--driver=native', '--force', '--no-logo']; if ($stack = $command->getSelectedStack()) { $args[] = $stack; From 14b63fc4930aa0d47f5f58219ad49de4462ad8a3 Mon Sep 17 00:00:00 2001 From: roble Date: Sat, 20 Jun 2026 09:39:31 +0100 Subject: [PATCH 2/6] feat: implement missingPrerequisites method to check for required tools in Docker and Native environments --- src/Console/Commands/InstallCommand.php | 10 +++ src/Environments/Contracts/Environment.php | 3 + src/Environments/DockerEnvironment.php | 24 +++++- src/Environments/NativeEnvironment.php | 16 ++++ .../Environments/DockerEnvironmentTest.php | 74 +++++++++++++++++++ .../Environments/NativeEnvironmentTest.php | 30 ++++++++ tests/Feature/InstallCommandTest.php | 54 ++++++++++++++ 7 files changed, 210 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php index aed48c5..ee42149 100644 --- a/src/Console/Commands/InstallCommand.php +++ b/src/Console/Commands/InstallCommand.php @@ -52,6 +52,16 @@ public function handle(): int } $driver = $this->resolveDriver(); + + $missing = $driver->missingPrerequisites(); + if (! empty($missing)) { + foreach ($missing as $message) { + $this->error($message); + } + + return self::FAILURE; + } + $this->promptForModules(); return $driver->run($this); diff --git a/src/Environments/Contracts/Environment.php b/src/Environments/Contracts/Environment.php index 034c9b8..425d634 100644 --- a/src/Environments/Contracts/Environment.php +++ b/src/Environments/Contracts/Environment.php @@ -10,5 +10,8 @@ public function name(): string; public function label(): string; + /** @return array Human-readable error messages for each unmet prerequisite; empty means all good. */ + public function missingPrerequisites(): array; + public function run(InstallCommand $command): int; } diff --git a/src/Environments/DockerEnvironment.php b/src/Environments/DockerEnvironment.php index 9cae4b6..432d4dc 100644 --- a/src/Environments/DockerEnvironment.php +++ b/src/Environments/DockerEnvironment.php @@ -19,6 +19,23 @@ public function label(): string return 'Docker'; } + public function missingPrerequisites(): array + { + $missing = []; + + if (! $this->commandExists('docker')) { + $missing[] = 'docker is not installed or not in PATH.'; + } elseif (! $this->dockerComposeAvailable()) { + $missing[] = '"docker compose" subcommand is not available. Ensure Docker Desktop or a Compose plugin is installed.'; + } + + if (! $this->commandExists('npm')) { + $missing[] = 'npm is not installed or not in PATH.'; + } + + return $missing; + } + public function run(InstallCommand $command): int { $this->publishStubs($command); @@ -174,7 +191,12 @@ public function buildContainerArgs(InstallCommand $command): array return $args; } - private function commandExists(string $name): bool + protected function dockerComposeAvailable(): bool + { + return (bool) shell_exec('docker compose version 2>/dev/null'); + } + + protected function commandExists(string $name): bool { return (bool) shell_exec("which {$name} 2>/dev/null"); } diff --git a/src/Environments/NativeEnvironment.php b/src/Environments/NativeEnvironment.php index 72ce6c3..3bb34b3 100644 --- a/src/Environments/NativeEnvironment.php +++ b/src/Environments/NativeEnvironment.php @@ -17,8 +17,24 @@ public function label(): string return 'Native PHP'; } + public function missingPrerequisites(): array + { + $missing = []; + + if (! $this->commandExists('composer')) { + $missing[] = 'composer is not installed or not in PATH.'; + } + + return $missing; + } + public function run(InstallCommand $command): int { return $command->install(); } + + protected function commandExists(string $name): bool + { + return (bool) shell_exec("which {$name} 2>/dev/null"); + } } diff --git a/tests/Feature/Environments/DockerEnvironmentTest.php b/tests/Feature/Environments/DockerEnvironmentTest.php index 189781a..156f0d6 100644 --- a/tests/Feature/Environments/DockerEnvironmentTest.php +++ b/tests/Feature/Environments/DockerEnvironmentTest.php @@ -127,6 +127,80 @@ public function test_multiple_flags_are_all_forwarded(): void $this->assertContains('--force', $args); } + // ------------------------------------------------------------------------- + // missingPrerequisites + // ------------------------------------------------------------------------- + + public function test_missing_prerequisites_returns_empty_when_all_tools_present(): void + { + $env = new class extends DockerEnvironment + { + protected function commandExists(string $name): bool { return true; } + + protected function dockerComposeAvailable(): bool { return true; } + }; + + $this->assertSame([], $env->missingPrerequisites()); + } + + public function test_missing_prerequisites_reports_docker_missing(): void + { + $env = new class extends DockerEnvironment + { + protected function commandExists(string $name): bool { return $name !== 'docker'; } + + protected function dockerComposeAvailable(): bool { return true; } + }; + + $missing = $env->missingPrerequisites(); + $this->assertCount(1, $missing); + $this->assertStringContainsString('docker', $missing[0]); + } + + public function test_missing_prerequisites_reports_docker_compose_missing(): void + { + $env = new class extends DockerEnvironment + { + protected function commandExists(string $name): bool { return true; } + + protected function dockerComposeAvailable(): bool { return false; } + }; + + $missing = $env->missingPrerequisites(); + $this->assertCount(1, $missing); + $this->assertStringContainsString('docker compose', $missing[0]); + } + + public function test_missing_prerequisites_reports_npm_missing(): void + { + $env = new class extends DockerEnvironment + { + protected function commandExists(string $name): bool { return $name !== 'npm'; } + + protected function dockerComposeAvailable(): bool { return true; } + }; + + $missing = $env->missingPrerequisites(); + $this->assertCount(1, $missing); + $this->assertStringContainsString('npm', $missing[0]); + } + + public function test_missing_prerequisites_skips_compose_check_when_docker_itself_missing(): void + { + $env = new class extends DockerEnvironment + { + protected function commandExists(string $name): bool { return false; } + + protected function dockerComposeAvailable(): bool { return false; } + }; + + $missing = $env->missingPrerequisites(); + // docker + npm missing; docker compose check is skipped via elseif + $this->assertCount(2, $missing); + $this->assertStringContainsString('docker', $missing[0]); + $this->assertStringContainsString('npm', $missing[1]); + } + // ------------------------------------------------------------------------- // run() failure propagation // ------------------------------------------------------------------------- diff --git a/tests/Feature/Environments/NativeEnvironmentTest.php b/tests/Feature/Environments/NativeEnvironmentTest.php index 5a09c7e..c4713b1 100644 --- a/tests/Feature/Environments/NativeEnvironmentTest.php +++ b/tests/Feature/Environments/NativeEnvironmentTest.php @@ -25,6 +25,36 @@ public function test_implements_environment_contract(): void $this->assertInstanceOf(Environment::class, new NativeEnvironment); } + // ------------------------------------------------------------------------- + // missingPrerequisites + // ------------------------------------------------------------------------- + + public function test_missing_prerequisites_returns_empty_when_composer_present(): void + { + $env = new class extends NativeEnvironment + { + protected function commandExists(string $name): bool { return true; } + }; + + $this->assertSame([], $env->missingPrerequisites()); + } + + public function test_missing_prerequisites_reports_composer_missing(): void + { + $env = new class extends NativeEnvironment + { + protected function commandExists(string $name): bool { return false; } + }; + + $missing = $env->missingPrerequisites(); + $this->assertCount(1, $missing); + $this->assertStringContainsString('composer', $missing[0]); + } + + // ------------------------------------------------------------------------- + // run() + // ------------------------------------------------------------------------- + public function test_run_delegates_to_install_and_returns_success(): void { $spy = (object) ['installCalled' => false]; diff --git a/tests/Feature/InstallCommandTest.php b/tests/Feature/InstallCommandTest.php index ed76e48..08b0b84 100644 --- a/tests/Feature/InstallCommandTest.php +++ b/tests/Feature/InstallCommandTest.php @@ -2,6 +2,7 @@ namespace Saucebase\Installer\Tests\Feature; +use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; use Saucebase\Installer\Console\Commands\InstallCommand; use Saucebase\Installer\Environments\Contracts\Environment; @@ -335,6 +336,59 @@ protected function displaySuccess(): void {} $this->artisan('saucebase:install vue --driver=native --all-modules')->assertSuccessful(); } + // ------------------------------------------------------------------------- + // missingPrerequisites gate in handle() + // ------------------------------------------------------------------------- + + public function test_handle_returns_failure_and_skips_run_when_prerequisites_not_met(): void + { + $spy = (object) ['runCalled' => false]; + + app()->bind(InstallCommand::class, function () use ($spy) { + return new class($spy) extends InstallCommand + { + public function __construct(private object $spy) + { + parent::__construct(); + } + + protected function isCI(): bool + { + return false; + } + + protected function resolveDriver(): Environment + { + $spy = $this->spy; + + return new class($spy) implements Environment + { + public function __construct(private object $spy) {} + + public function name(): string { return 'fake'; } + + public function label(): string { return 'Fake'; } + + public function missingPrerequisites(): array + { + return ['fake-tool is not installed or not in PATH.']; + } + + public function run(InstallCommand $command): int + { + $this->spy->runCalled = true; + + return Command::SUCCESS; + } + }; + } + }; + }); + + $this->artisan('saucebase:install vue --all-modules')->assertFailed(); + $this->assertFalse($spy->runCalled, 'run() must not be called when prerequisites are not met'); + } + // ------------------------------------------------------------------------- // handleCIInstallation exit codes // ------------------------------------------------------------------------- From a6c2704ee816dc5f33dfa5e7b612eb62aa9c716f Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 24 Jun 2026 20:55:08 +0100 Subject: [PATCH 3/6] fix: enhance Docker environment setup and update InstallCommand to ensure .env file is configured --- src/Console/Commands/InstallCommand.php | 9 +- src/Environments/DockerEnvironment.php | 79 +++++++++- .../Environments/DockerEnvironmentTest.php | 137 ++++++++++++++++++ tests/Feature/InstallCommandTest.php | 8 +- 4 files changed, 221 insertions(+), 12 deletions(-) diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php index ee42149..9112432 100644 --- a/src/Console/Commands/InstallCommand.php +++ b/src/Console/Commands/InstallCommand.php @@ -82,8 +82,11 @@ protected function resolveDriver(): Environment { $name = $this->option('driver') ?? select( label: 'How would you like to run Saucebase?', - options: ['docker' => 'Docker (recommended)', 'native' => 'Native PHP'], - default: 'docker', + options: [ + 'native' => 'Native PHP - minimal setup, ideal for exploring', + 'docker' => 'Docker - recommended for real projects: MySQL, Redis, Mailpit, HTTPS', + ], + default: 'native', ); return match ($name) { @@ -241,7 +244,7 @@ protected function handleCIInstallation(): int return self::SUCCESS; } - protected function ensureEnvFile(): bool + public function ensureEnvFile(): bool { if (file_exists(base_path('.env'))) { return true; diff --git a/src/Environments/DockerEnvironment.php b/src/Environments/DockerEnvironment.php index 432d4dc..a01f648 100644 --- a/src/Environments/DockerEnvironment.php +++ b/src/Environments/DockerEnvironment.php @@ -41,6 +41,12 @@ public function run(InstallCommand $command): int $this->publishStubs($command); $this->generateSsl($command); + if (! $command->ensureEnvFile()) { + return InstallCommand::FAILURE; + } + + $this->setDockerEnvDefaults($command); + if (! $this->startDocker($command)) { return InstallCommand::FAILURE; } @@ -97,12 +103,15 @@ protected function generateSsl(InstallCommand $command): void protected function startDocker(InstallCommand $command): bool { - $command->info('Starting Docker services...'); - (new Process(['docker', 'compose', 'restart']))->run(); + $command->info('Starting Docker services (this may take a few minutes while pulling images and starting containers)...'); - $up = new Process(['docker', 'compose', 'up', '-d', '--wait']); - $up->setTimeout(120); - $up->run(); + $restart = new Process(['docker', 'compose', 'restart']); + $restart->setTimeout(60); + $restart->run(); + + $up = new Process(['docker', 'compose', 'up', '-d', '--wait', '--build']); + $up->setTimeout(30 * 60); // 30 minutes — first run pulls images + builds layers + $up->run(fn ($_type, $buffer) => $command->line(trim($buffer))); if (! $up->isSuccessful()) { $command->error('Docker failed to start: '.$up->getErrorOutput()); @@ -191,6 +200,66 @@ public function buildContainerArgs(InstallCommand $command): array return $args; } + protected function setDockerEnvDefaults(InstallCommand $command): void + { + $path = base_path('.env'); + $original = file_get_contents($path); + + if ($original === false) { + return; + } + + $modified = $this->applyDockerEnvDefaults($original); + + if ($modified !== $original) { + file_put_contents($path, $modified); + $command->info('Docker database credentials written to .env.'); + } + } + + protected function applyDockerEnvDefaults(string $env): string + { + $slug = 'saucebase'; + if (preg_match('/^APP_SLUG=([^\s]+)/m', $env, $m)) { + $slug = trim($m[1], "\"'"); + } + + // Docker always needs mysql, not sqlite + if (preg_match('/^DB_CONNECTION=(.*)$/m', $env, $m) && trim($m[1]) !== 'mysql') { + $env = preg_replace('/^DB_CONNECTION=.*$/m', 'DB_CONNECTION=mysql', $env); + } elseif (! preg_match('/^DB_CONNECTION=/m', $env)) { + $env .= "\nDB_CONNECTION=mysql"; + } + + // Docker routes mail through the Mailpit container via SMTP + if (preg_match('/^MAIL_MAILER=(.*)$/m', $env, $m) && trim($m[1]) !== 'smtp') { + $env = preg_replace('/^MAIL_MAILER=.*$/m', 'MAIL_MAILER=smtp', $env); + } elseif (! preg_match('/^MAIL_MAILER=/m', $env)) { + $env .= "\nMAIL_MAILER=smtp"; + } + + // Set missing or blank values; respect anything the user has already configured + $defaults = [ + 'DB_HOST' => 'mysql', + 'DB_PORT' => '3306', + 'DB_DATABASE' => $slug, + 'DB_USERNAME' => $slug, + 'DB_PASSWORD' => 'secret', + ]; + + foreach ($defaults as $key => $value) { + if (preg_match('/^'.preg_quote($key, '/').'=(.*)$/m', $env, $m)) { + if (trim($m[1]) === '') { + $env = preg_replace('/^'.preg_quote($key, '/').'=.*$/m', "{$key}={$value}", $env); + } + } else { + $env .= "\n{$key}={$value}"; + } + } + + return $env; + } + protected function dockerComposeAvailable(): bool { return (bool) shell_exec('docker compose version 2>/dev/null'); diff --git a/tests/Feature/Environments/DockerEnvironmentTest.php b/tests/Feature/Environments/DockerEnvironmentTest.php index 156f0d6..2d75b78 100644 --- a/tests/Feature/Environments/DockerEnvironmentTest.php +++ b/tests/Feature/Environments/DockerEnvironmentTest.php @@ -217,6 +217,8 @@ protected function publishStubs(InstallCommand $command): void {} protected function generateSsl(InstallCommand $command): void {} + protected function setDockerEnvDefaults(InstallCommand $command): void {} + protected function startDocker(InstallCommand $command): bool { return false; @@ -248,6 +250,8 @@ protected function publishStubs(InstallCommand $command): void {} protected function generateSsl(InstallCommand $command): void {} + protected function setDockerEnvDefaults(InstallCommand $command): void {} + protected function startDocker(InstallCommand $command): bool { return true; @@ -270,10 +274,143 @@ protected function runInstallInContainer(InstallCommand $command): void $this->assertFalse($spy->installCalled, 'in-container install must not run when composer install fails'); } + // ------------------------------------------------------------------------- + // applyDockerEnvDefaults + // ------------------------------------------------------------------------- + + public function test_replaces_sqlite_connection_with_mysql(): void + { + $result = $this->applyDefaults("DB_CONNECTION=sqlite\n"); + + $this->assertStringContainsString('DB_CONNECTION=mysql', $result); + $this->assertStringNotContainsString('DB_CONNECTION=sqlite', $result); + } + + public function test_leaves_mysql_connection_unchanged(): void + { + $input = "DB_CONNECTION=mysql\nDB_HOST=mysql\nDB_PORT=3306\nDB_DATABASE=myapp\nDB_USERNAME=myapp\nDB_PASSWORD=secret\nMAIL_MAILER=smtp\n"; + $result = $this->applyDefaults($input); + + $this->assertSame($input, $result); + } + + public function test_appends_db_connection_when_missing(): void + { + $result = $this->applyDefaults("APP_NAME=Test\n"); + + $this->assertStringContainsString('DB_CONNECTION=mysql', $result); + } + + public function test_uses_app_slug_for_db_database_and_username(): void + { + $result = $this->applyDefaults("APP_SLUG=myproject\nDB_CONNECTION=sqlite\n"); + + $this->assertStringContainsString('DB_DATABASE=myproject', $result); + $this->assertStringContainsString('DB_USERNAME=myproject', $result); + } + + public function test_falls_back_to_saucebase_slug_when_app_slug_missing(): void + { + $result = $this->applyDefaults("DB_CONNECTION=sqlite\n"); + + $this->assertStringContainsString('DB_DATABASE=saucebase', $result); + $this->assertStringContainsString('DB_USERNAME=saucebase', $result); + } + + public function test_sets_blank_db_vars_to_defaults(): void + { + $input = "DB_CONNECTION=sqlite\nDB_HOST=\nDB_PORT=\nDB_DATABASE=\nDB_USERNAME=\nDB_PASSWORD=\n"; + $result = $this->applyDefaults($input); + + $this->assertStringContainsString('DB_HOST=mysql', $result); + $this->assertStringContainsString('DB_PORT=3306', $result); + $this->assertStringContainsString('DB_PASSWORD=secret', $result); + } + + public function test_does_not_overwrite_existing_db_values(): void + { + $input = "DB_CONNECTION=mysql\nDB_HOST=custom-host\nDB_PORT=3307\nDB_DATABASE=mydb\nDB_USERNAME=myuser\nDB_PASSWORD=mypass\n"; + $result = $this->applyDefaults($input); + + $this->assertStringContainsString('DB_HOST=custom-host', $result); + $this->assertStringContainsString('DB_PORT=3307', $result); + $this->assertStringContainsString('DB_DATABASE=mydb', $result); + $this->assertStringContainsString('DB_USERNAME=myuser', $result); + $this->assertStringContainsString('DB_PASSWORD=mypass', $result); + } + + public function test_appends_missing_db_vars(): void + { + $result = $this->applyDefaults("DB_CONNECTION=sqlite\n"); + + $this->assertStringContainsString('DB_HOST=mysql', $result); + $this->assertStringContainsString('DB_PORT=3306', $result); + $this->assertStringContainsString('DB_PASSWORD=secret', $result); + } + + public function test_replaces_log_mailer_with_smtp(): void + { + $result = $this->applyDefaults("MAIL_MAILER=log\n"); + + $this->assertStringContainsString('MAIL_MAILER=smtp', $result); + $this->assertStringNotContainsString('MAIL_MAILER=log', $result); + } + + public function test_leaves_smtp_mailer_unchanged(): void + { + $input = "MAIL_MAILER=smtp\n"; + $result = $this->applyDefaults($input); + + $this->assertStringContainsString('MAIL_MAILER=smtp', $result); + } + + public function test_appends_mail_mailer_when_missing(): void + { + $result = $this->applyDefaults("APP_NAME=Test\n"); + + $this->assertStringContainsString('MAIL_MAILER=smtp', $result); + } + + public function test_real_env_example_pattern_produces_valid_docker_env(): void + { + $input = implode("\n", [ + 'APP_SLUG=acme', + 'DB_CONNECTION=sqlite', + '# DB_HOST=localhost', + '# DB_DATABASE=${APP_SLUG}', + '# DB_USERNAME=${APP_SLUG}', + '# DB_PASSWORD=secret', + 'MAIL_MAILER=log', + '', + ]); + + $result = $this->applyDefaults($input); + + $this->assertStringContainsString('DB_CONNECTION=mysql', $result); + $this->assertStringContainsString('DB_DATABASE=acme', $result); + $this->assertStringContainsString('DB_USERNAME=acme', $result); + $this->assertStringContainsString('DB_HOST=mysql', $result); + $this->assertStringContainsString('DB_PASSWORD=secret', $result); + $this->assertStringContainsString('MAIL_MAILER=smtp', $result); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- + private function applyDefaults(string $env): string + { + $exposed = new class extends DockerEnvironment + { + public function applyDockerEnvDefaults(string $env): string + { + return parent::applyDockerEnvDefaults($env); + } + }; + + return $exposed->applyDockerEnvDefaults($env); + } + /** * @param string[] $modules * @param array $options diff --git a/tests/Feature/InstallCommandTest.php b/tests/Feature/InstallCommandTest.php index 08b0b84..05b0a1e 100644 --- a/tests/Feature/InstallCommandTest.php +++ b/tests/Feature/InstallCommandTest.php @@ -196,7 +196,7 @@ protected function isCI(): bool return false; } - protected function ensureEnvFile(): bool + public function ensureEnvFile(): bool { return true; } @@ -270,7 +270,7 @@ protected function resolveDriver(): Environment return new NativeEnvironment; } - protected function ensureEnvFile(): bool + public function ensureEnvFile(): bool { return true; } @@ -311,7 +311,7 @@ protected function isCI(): bool return false; } - protected function ensureEnvFile(): bool + public function ensureEnvFile(): bool { return true; } @@ -422,7 +422,7 @@ public function test_install_returns_failure_and_skips_modules_when_database_set { public object $spy; - protected function ensureEnvFile(): bool + public function ensureEnvFile(): bool { return true; } From 707e2fffa1a3dc747d6fd6b4b37499c29d3e2312 Mon Sep 17 00:00:00 2001 From: roble <3231587+roble@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:56:11 +0000 Subject: [PATCH 4/6] PHP Linting (Pint) --- src/Environments/DockerEnvironment.php | 4 +- .../Environments/DockerEnvironmentTest.php | 50 +++++++++++++++---- .../Environments/NativeEnvironmentTest.php | 10 +++- tests/Feature/InstallCommandTest.php | 10 +++- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/Environments/DockerEnvironment.php b/src/Environments/DockerEnvironment.php index a01f648..3a2b827 100644 --- a/src/Environments/DockerEnvironment.php +++ b/src/Environments/DockerEnvironment.php @@ -240,8 +240,8 @@ protected function applyDockerEnvDefaults(string $env): string // Set missing or blank values; respect anything the user has already configured $defaults = [ - 'DB_HOST' => 'mysql', - 'DB_PORT' => '3306', + 'DB_HOST' => 'mysql', + 'DB_PORT' => '3306', 'DB_DATABASE' => $slug, 'DB_USERNAME' => $slug, 'DB_PASSWORD' => 'secret', diff --git a/tests/Feature/Environments/DockerEnvironmentTest.php b/tests/Feature/Environments/DockerEnvironmentTest.php index 2d75b78..31e0a86 100644 --- a/tests/Feature/Environments/DockerEnvironmentTest.php +++ b/tests/Feature/Environments/DockerEnvironmentTest.php @@ -135,9 +135,15 @@ public function test_missing_prerequisites_returns_empty_when_all_tools_present( { $env = new class extends DockerEnvironment { - protected function commandExists(string $name): bool { return true; } + protected function commandExists(string $name): bool + { + return true; + } - protected function dockerComposeAvailable(): bool { return true; } + protected function dockerComposeAvailable(): bool + { + return true; + } }; $this->assertSame([], $env->missingPrerequisites()); @@ -147,9 +153,15 @@ public function test_missing_prerequisites_reports_docker_missing(): void { $env = new class extends DockerEnvironment { - protected function commandExists(string $name): bool { return $name !== 'docker'; } + protected function commandExists(string $name): bool + { + return $name !== 'docker'; + } - protected function dockerComposeAvailable(): bool { return true; } + protected function dockerComposeAvailable(): bool + { + return true; + } }; $missing = $env->missingPrerequisites(); @@ -161,9 +173,15 @@ public function test_missing_prerequisites_reports_docker_compose_missing(): voi { $env = new class extends DockerEnvironment { - protected function commandExists(string $name): bool { return true; } + protected function commandExists(string $name): bool + { + return true; + } - protected function dockerComposeAvailable(): bool { return false; } + protected function dockerComposeAvailable(): bool + { + return false; + } }; $missing = $env->missingPrerequisites(); @@ -175,9 +193,15 @@ public function test_missing_prerequisites_reports_npm_missing(): void { $env = new class extends DockerEnvironment { - protected function commandExists(string $name): bool { return $name !== 'npm'; } + protected function commandExists(string $name): bool + { + return $name !== 'npm'; + } - protected function dockerComposeAvailable(): bool { return true; } + protected function dockerComposeAvailable(): bool + { + return true; + } }; $missing = $env->missingPrerequisites(); @@ -189,9 +213,15 @@ public function test_missing_prerequisites_skips_compose_check_when_docker_itsel { $env = new class extends DockerEnvironment { - protected function commandExists(string $name): bool { return false; } + protected function commandExists(string $name): bool + { + return false; + } - protected function dockerComposeAvailable(): bool { return false; } + protected function dockerComposeAvailable(): bool + { + return false; + } }; $missing = $env->missingPrerequisites(); diff --git a/tests/Feature/Environments/NativeEnvironmentTest.php b/tests/Feature/Environments/NativeEnvironmentTest.php index c4713b1..3f585b2 100644 --- a/tests/Feature/Environments/NativeEnvironmentTest.php +++ b/tests/Feature/Environments/NativeEnvironmentTest.php @@ -33,7 +33,10 @@ public function test_missing_prerequisites_returns_empty_when_composer_present() { $env = new class extends NativeEnvironment { - protected function commandExists(string $name): bool { return true; } + protected function commandExists(string $name): bool + { + return true; + } }; $this->assertSame([], $env->missingPrerequisites()); @@ -43,7 +46,10 @@ public function test_missing_prerequisites_reports_composer_missing(): void { $env = new class extends NativeEnvironment { - protected function commandExists(string $name): bool { return false; } + protected function commandExists(string $name): bool + { + return false; + } }; $missing = $env->missingPrerequisites(); diff --git a/tests/Feature/InstallCommandTest.php b/tests/Feature/InstallCommandTest.php index 05b0a1e..d6b3522 100644 --- a/tests/Feature/InstallCommandTest.php +++ b/tests/Feature/InstallCommandTest.php @@ -365,9 +365,15 @@ protected function resolveDriver(): Environment { public function __construct(private object $spy) {} - public function name(): string { return 'fake'; } + public function name(): string + { + return 'fake'; + } - public function label(): string { return 'Fake'; } + public function label(): string + { + return 'Fake'; + } public function missingPrerequisites(): array { From f93facfb4bfe2837529e6ac1ebb55d6e9db449fc Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 24 Jun 2026 21:00:02 +0100 Subject: [PATCH 5/6] docs: add test badge to README for CI visibility --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b90e16b..c7e6f95 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # saucebase/installer +![Tests](https://github.com/saucebase-dev/installer/actions/workflows/php.yml/badge.svg) + Dev-environment installer for [Saucebase](https://github.com/saucebase-dev/saucebase) applications. Provides two Artisan commands: `saucebase:install` bootstraps the entire dev environment (Docker, dependencies, database, frontend), and `saucebase:stack` switches the frontend framework after the environment is running. From 7caf28cf7747da5ebda91cb2915cffc412113d2a Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 24 Jun 2026 21:06:26 +0100 Subject: [PATCH 6/6] fix: update .env file verification in InstallCommand and add test stubs --- src/Console/Commands/InstallCommand.php | 2 +- tests/Feature/InstallCommandTest.php | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php index 9112432..69ff996 100644 --- a/src/Console/Commands/InstallCommand.php +++ b/src/Console/Commands/InstallCommand.php @@ -229,7 +229,7 @@ protected function handleCIInstallation(): int { $this->info('CI environment detected - running minimal setup...'); - $envOk = file_exists(base_path('.env')); + $envOk = $this->ensureEnvFile(); $keyOk = ! empty(config('app.key')); $this->components->task('Verifying .env', fn () => $envOk); diff --git a/tests/Feature/InstallCommandTest.php b/tests/Feature/InstallCommandTest.php index d6b3522..d45bd00 100644 --- a/tests/Feature/InstallCommandTest.php +++ b/tests/Feature/InstallCommandTest.php @@ -408,10 +408,14 @@ protected function isCI(): bool { return true; } + + public function ensureEnvFile(): bool + { + return false; + } }; }); - // No .env in the Testbench app root — task will return false $this->artisan('saucebase:install vue')->assertFailed(); } @@ -428,6 +432,11 @@ public function test_install_returns_failure_and_skips_modules_when_database_set { public object $spy; + protected function isCI(): bool + { + return false; + } + public function ensureEnvFile(): bool { return true;