From 4fd0b63906d51aa7cffce6429f32244e8f780943 Mon Sep 17 00:00:00 2001 From: Mischa Sigtermans Date: 2025年8月16日 13:45:51 +0800 Subject: [PATCH] Add environment variable configuration for boost:install command --- README.md | 61 ++++ config/boost.php | 19 ++ src/Console/InstallCommand.php | 245 +++++++++++++-- .../Console/InstallCommandEnvironmentTest.php | 297 ++++++++++++++++++ 4 files changed, 598 insertions(+), 24 deletions(-) create mode 100644 tests/Feature/Console/InstallCommandEnvironmentTest.php diff --git a/README.md b/README.md index f0c030c7..85cc0bfe 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,67 @@ php artisan boost:install Once Laravel Boost has been installed, you're ready to start coding with Cursor, Claude Code, or your AI agent of choice. +## Configuration + +### Non-Interactive Installation + +For a faster installation workflow when you already know your preferences, you can skip the interactive prompts using the `--no-interaction` or `-n` flag: + +```bash +php artisan boost:install -n +``` + +### Environment Variables + +You can configure the installation behavior using environment variables in your `.env` file: + +```bash +# Control what gets installed +BOOST_MCP_SERVER=true # Install MCP server (default: true) +BOOST_AI_GUIDELINES=true # Install AI guidelines (default: true) +BOOST_HERD=false # Install Herd MCP server (default: false) + +# Pre-select agents and editors (comma-separated) +BOOST_AGENTS=claudecode # Agents for AI guidelines +BOOST_EDITORS=claudecode # Editors for MCP installation + +# Test enforcement in AI guidelines +BOOST_ENFORCE_TESTS=true # Always create tests (default: auto-detect) +``` + +**Available Agents:** +- `claudecode` - Claude Code +- `cursor` - Cursor +- `copilot` - GitHub Copilot (no MCP support) +- `phpstorm` - PhpStorm/Junie + +**Available Editors:** +- `claudecode` - Claude Code +- `cursor` - Cursor +- `phpstorm` - PhpStorm +- `vscode` - VS Code (MCP only) + +**Special Values:** +- Set `BOOST_AI_GUIDELINES=false` to skip AI guidelines installation entirely +- Environment variables work as **defaults** in interactive mode, or **enforced values** with `--no-interaction` + +### Configuration File + +You can also set defaults in your `config/boost.php` file: + +```php +return [ + 'install' => [ + 'mcp_server' => true, + 'ai_guidelines' => true, + 'herd' => false, + 'enforce_tests' => true, + 'agents' => 'claudecode', + 'editors' => 'claudecode', + ], +]; +``` + ## Available MCP Tools | Name | Notes | diff --git a/config/boost.php b/config/boost.php index 95973c7c..cac40cc7 100644 --- a/config/boost.php +++ b/config/boost.php @@ -16,4 +16,23 @@ 'browser_logs_watcher' => env('BOOST_BROWSER_LOGS_WATCHER', true), + /* + |-------------------------------------------------------------------------- + | Installation Defaults + |-------------------------------------------------------------------------- + | + | These options control the default behavior during boost:install command. + | You can override these via environment variables or use them as defaults + | for non-interactive installations. + */ + + 'install' => [ + 'mcp_server' => env('BOOST_MCP_SERVER', true), + 'ai_guidelines' => env('BOOST_AI_GUIDELINES', true), + 'herd' => env('BOOST_HERD', false), + 'enforce_tests' => env('BOOST_ENFORCE_TESTS'), + 'agents' => env('BOOST_AGENTS'), + 'editors' => env('BOOST_EDITORS'), + ], + ]; diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 3d341d4b..759ac9c8 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -65,6 +65,8 @@ class InstallCommand extends Command private string $redCross; + private bool $isInteractive = true; + public function handle(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, Terminal $terminal): void { $this->bootstrap($codeEnvironmentsDetector, $herd, $terminal); @@ -90,6 +92,8 @@ private function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, H $this->selectedTargetMcpClient = collect(); $this->projectName = config('app.name'); + + $this->isInteractive = ! $this->option('no-interaction'); } private function displayBoostHeader(): void @@ -114,16 +118,18 @@ private function boostLogo(): string private function discoverEnvironment(): void { - $this->systemInstalledCodeEnvironments = $this->codeEnvironmentsDetector->discoverSystemInstalledCodeEnvironments(); - $this->projectInstalledCodeEnvironments = $this->codeEnvironmentsDetector->discoverProjectInstalledCodeEnvironments(base_path()); + $this->systemInstalledCodeEnvironments = $this->codeEnvironmentsDetector + ->discoverSystemInstalledCodeEnvironments(); + $this->projectInstalledCodeEnvironments = $this->codeEnvironmentsDetector + ->discoverProjectInstalledCodeEnvironments(base_path()); } private function collectInstallationPreferences(): void { $this->selectedBoostFeatures = $this->selectBoostFeatures(); + $this->enforceTests = $this->determineTestEnforcement(ask: $this->isInteractive); $this->selectedTargetMcpClient = $this->selectTargetMcpClients(); $this->selectedTargetAgents = $this->selectTargetAgents(); - $this->enforceTests = $this->determineTestEnforcement(ask: false); } private function performInstallation(): void @@ -201,6 +207,26 @@ private function hyperlink(string $label, string $url): string */ protected function determineTestEnforcement(bool $ask = true): bool { + if ($ask) { + $envValue = config('boost.install.enforce_tests'); + $default = 'Yes'; + + if ($envValue !== null && $envValue !== '') { + $default = filter_var($envValue, FILTER_VALIDATE_BOOLEAN) ? 'Yes' : 'No'; + } + + return select( + label: 'Should AI always create tests?', + options: ['Yes', 'No'], + default: $default + ) === 'Yes'; + } + + $envValue = config('boost.install.enforce_tests'); + if ($envValue !== null && $envValue !== '') { + return filter_var($envValue, FILTER_VALIDATE_BOOLEAN); + } + $hasMinimumTests = false; if (file_exists(base_path('vendor/bin/phpunit'))) { @@ -215,14 +241,6 @@ protected function determineTestEnforcement(bool $ask = true): bool ->count()>= self::MIN_TEST_COUNT; } - if (! $hasMinimumTests && $ask) { - $hasMinimumTests = select( - label: 'Should AI always create tests?', - options: ['Yes', 'No'], - default: 'Yes' - ) === 'Yes'; - } - return $hasMinimumTests; } @@ -231,15 +249,45 @@ protected function determineTestEnforcement(bool $ask = true): bool */ private function selectBoostFeatures(): Collection { - $defaultInstallOptions = ['mcp_server', 'ai_guidelines']; + if (! $this->isInteractive) { + $features = collect(); + + if (config('boost.install.mcp_server')) { + $features->push('mcp_server'); + } + + if (config('boost.install.ai_guidelines')) { + $features->push('ai_guidelines'); + } + + if ($this->herd->isMcpAvailable() && config('boost.install.herd')) { + $features->push('herd_mcp'); + } + + return $features; + } + + $defaultInstallOptions = []; $installOptions = [ 'mcp_server' => 'Boost MCP Server (with 15+ tools)', 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', ]; + if (config('boost.install.mcp_server')) { + $defaultInstallOptions[] = 'mcp_server'; + } + + if (config('boost.install.ai_guidelines')) { + $defaultInstallOptions[] = 'ai_guidelines'; + } + if ($this->herd->isMcpAvailable()) { $installOptions['herd_mcp'] = 'Herd MCP Server'; + if (config('boost.install.herd')) { + $defaultInstallOptions[] = 'herd_mcp'; + } + return collect(multiselect( label: 'What do you want to install?', options: $installOptions, @@ -277,6 +325,38 @@ private function selectTargetMcpClients(): Collection return collect(); } + if (! $this->isInteractive) { + $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); + $availableClients = $allEnvironments->filter(function (CodeEnvironment $environment) { + return $environment instanceof McpClient; + }); + + $envDefaults = $this->getEnvironmentDefaults(McpClient::class, $availableClients); + if (! empty($envDefaults)) { + return collect($envDefaults)->map(fn ($className) => $availableClients + ->first(fn ($env) => get_class($env) === $className) + ); + } + + $installedEnvNames = array_unique(array_merge( + $this->projectInstalledCodeEnvironments, + $this->systemInstalledCodeEnvironments + )); + + foreach ($installedEnvNames as $envKey) { + $matchingEnv = $availableClients + ->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name()) + ); + if ($matchingEnv) { + return collect([$matchingEnv]); + } + } + + $firstClient = $availableClients->first(); + + return $firstClient ? collect([$firstClient]) : collect(); + } + return $this->selectCodeEnvironments( McpClient::class, sprintf('Which code editors do you use to work on %s?', $this->projectName) @@ -292,6 +372,38 @@ private function selectTargetAgents(): Collection return collect(); } + if (! $this->isInteractive) { + $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); + $availableAgents = $allEnvironments->filter(function (CodeEnvironment $environment) { + return $environment instanceof Agent; + }); + + $envDefaults = $this->getEnvironmentDefaults(Agent::class, $availableAgents); + if (! empty($envDefaults)) { + return collect($envDefaults)->map(fn ($className) => $availableAgents + ->first(fn ($env) => get_class($env) === $className) + ); + } + + $installedEnvNames = array_unique(array_merge( + $this->projectInstalledCodeEnvironments, + $this->systemInstalledCodeEnvironments + )); + + foreach ($installedEnvNames as $envKey) { + $matchingEnv = $availableAgents + ->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name()) + ); + if ($matchingEnv) { + return collect([$matchingEnv]); + } + } + + $firstAgent = $availableAgents->first(); + + return $firstAgent ? collect([$firstAgent]) : collect(); + } + return $this->selectCodeEnvironments( Agent::class, sprintf('Which agents need AI guidelines for %s?', $this->projectName) @@ -342,29 +454,29 @@ private function selectCodeEnvironments(string $contractClass, string $label): C )); foreach ($installedEnvNames as $envKey) { - $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name())); + $matchingEnv = $availableEnvironments + ->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name())); + if ($matchingEnv) { $detectedClasses[] = get_class($matchingEnv); } } + $envDefaults = $this->getEnvironmentDefaults($contractClass, $availableEnvironments); + $defaultClasses = $envDefaults !== null ? $envDefaults : $detectedClasses; + $hintText = $this->generateHintText($detectedClasses, $availableEnvironments, $config, $contractClass); + $selectedClasses = collect(multiselect( label: $label, options: $options->toArray(), - default: array_unique($detectedClasses), + default: array_unique($defaultClasses), scroll: $config['scroll'], required: $config['required'], - hint: empty($detectedClasses) ? '' : sprintf('Auto-detected %s for you', - Arr::join(array_map(function ($className) use ($availableEnvironments, $config) { - $env = $availableEnvironments->first(fn ($env) => get_class($env) === $className); - $displayMethod = $config['displayMethod']; - - return $env->{$displayMethod}(); - }, $detectedClasses), ', ', ' & ') - ) + hint: $hintText ))->sort(); - return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env) => get_class($env) === $className)); + return $selectedClasses->map(fn ($className) => $availableEnvironments + ->first(fn ($env) => get_class($env) === $className)); } private function installGuidelines(): void @@ -397,7 +509,9 @@ private function installGuidelines(): void $failed = []; $composedAiGuidelines = $composer->compose(); - $longestAgentName = max(1, ...$this->selectedTargetAgents->map(fn ($agent) => Str::length($agent->agentName()))->toArray()); + $longestAgentName = max(1, ...$this->selectedTargetAgents + ->map(fn ($agent) => Str::length($agent->agentName()))->toArray()); + /** @var CodeEnvironment $agent */ foreach ($this->selectedTargetAgents as $agent) { $agentName = $agent->agentName(); @@ -545,4 +659,87 @@ private function detectLocalization(): bool /** @phpstan-ignore-next-line */ return $actuallyUsing && is_dir(base_path('lang')); } + + /** + * Get environment variable defaults for the given contract class. + */ + private function getEnvironmentDefaults(string $contractClass, Collection $availableEnvironments): ?array + { + $envVar = match ($contractClass) { + Agent::class => config('boost.install.agents'), + McpClient::class => config('boost.install.editors'), + default => null, + }; + + if ($envVar === null) { + return null; + } + + if ($contractClass === Agent::class && ($envVar === false || $envVar === 'false' || $envVar === '0')) { + return []; + } + + if (! $envVar) { + return null; + } + + $names = array_map('trim', explode(',', $envVar)); + $defaultClasses = []; + + foreach ($names as $name) { + $env = $availableEnvironments->first(function (CodeEnvironment $env) use ($name, $contractClass) { + $nameMatches = strtolower($env->name()) === strtolower($name); + + if ($contractClass === Agent::class && $env instanceof Agent) { + return $nameMatches || strtolower($env->agentName()) === strtolower($name); + } + + if ($contractClass === McpClient::class && $env instanceof McpClient) { + return $nameMatches || strtolower($env->mcpClientName()) === strtolower($name); + } + + return $nameMatches; + }); + + if ($env) { + $defaultClasses[] = get_class($env); + } + } + + return $defaultClasses; + } + + /** + * Generate hint text for multiselect prompts. + */ + private function generateHintText(array $detectedClasses, Collection $availableEnvironments, array $config, string $contractClass): string + { + $envVar = match ($contractClass) { + Agent::class => config('boost.install.agents'), + McpClient::class => config('boost.install.editors'), + default => null, + }; + + if ($envVar !== null) { + if ($contractClass === Agent::class && ($envVar === false || $envVar === 'false' || $envVar === '0')) { + return 'None selected via environment variable'; + } + + return 'Pre-selected from environment variable'; + } + + if (empty($detectedClasses)) { + return ''; + } + + return sprintf('Auto-detected %s for you', + Arr::join(array_map(function ($className) use ($availableEnvironments, $config) { + $env = $availableEnvironments->first(fn ($env) => get_class($env) === $className); + $displayMethod = $config['displayMethod']; + + return $env->{$displayMethod}(); + }, $detectedClasses), ', ', ' & ') + ); + } + } diff --git a/tests/Feature/Console/InstallCommandEnvironmentTest.php b/tests/Feature/Console/InstallCommandEnvironmentTest.php new file mode 100644 index 00000000..d3d3bc63 --- /dev/null +++ b/tests/Feature/Console/InstallCommandEnvironmentTest.php @@ -0,0 +1,297 @@ + 'Boost MCP Server (with 15+ tools)', + 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', + 'style_guidelines' => 'Laravel Style AI Guidelines', + ]; + + // This simulates what selectBoostFeatures() does in interactive mode + $result = \Laravel\Prompts\multiselect( + label: 'What do you want to install?', + options: $installOptions, + default: ['mcp_server', 'ai_guidelines'], // Pre-selected via config + required: true, + ); + + // Verify the defaults were accepted + $this->assertIsArray($result); + $this->assertContains('mcp_server', $result); + $this->assertContains('ai_guidelines', $result); + $this->assertNotContains('style_guidelines', $result); + } + + /** + * Test that when no environment variables are set, auto-detection works. + */ + public function test_no_environment_variables_uses_defaults(): void + { + // Clear all config (simulating no env vars set) + Config::set('boost.install.mcp_server', true); // Default + Config::set('boost.install.ai_guidelines', true); // Default + Config::set('boost.install.herd', false); // Default + + Prompt::fake([ + Key::ENTER, // Accept defaults + ]); + + $installOptions = [ + 'mcp_server' => 'Boost MCP Server (with 15+ tools)', + 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', + ]; + + $result = \Laravel\Prompts\multiselect( + label: 'What do you want to install?', + options: $installOptions, + default: ['mcp_server', 'ai_guidelines'], + required: true, + ); + + $this->assertContains('mcp_server', $result); + $this->assertContains('ai_guidelines', $result); + } + + /** + * Test that environment variables can disable features. + */ + public function test_environment_variables_can_disable_features(): void + { + // Set config to disable ai_guidelines (like BOOST_AI_GUIDELINES=false) + Config::set('boost.install.mcp_server', true); + Config::set('boost.install.ai_guidelines', false); + + Prompt::fake([ + Key::ENTER, // Accept pre-selected options (should only be mcp_server) + ]); + + $installOptions = [ + 'mcp_server' => 'Boost MCP Server (with 15+ tools)', + 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', + ]; + + // Only mcp_server should be pre-selected + $result = \Laravel\Prompts\multiselect( + label: 'What do you want to install?', + options: $installOptions, + default: ['mcp_server'], // Only mcp_server pre-selected + required: true, + ); + + $this->assertContains('mcp_server', $result); + $this->assertNotContains('ai_guidelines', $result); + } + + /** + * Test that user can override environment variable defaults. + */ + public function test_user_can_override_environment_defaults(): void + { + // Even though config says ai_guidelines=true, user can toggle it off + Config::set('boost.install.mcp_server', true); + Config::set('boost.install.ai_guidelines', true); + + Prompt::fake([ + Key::DOWN, // Move to ai_guidelines + Key::SPACE, // Toggle ai_guidelines off + Key::ENTER, // Submit + ]); + + $installOptions = [ + 'mcp_server' => 'Boost MCP Server (with 15+ tools)', + 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', + ]; + + $result = \Laravel\Prompts\multiselect( + label: 'What do you want to install?', + options: $installOptions, + default: ['mcp_server', 'ai_guidelines'], // Both pre-selected + required: true, + ); + + // User toggled off ai_guidelines, so only mcp_server should remain + $this->assertContains('mcp_server', $result); + $this->assertNotContains('ai_guidelines', $result); + } + + /** + * Test agent selection with environment variable defaults. + */ + public function test_agent_selection_respects_environment_defaults(): void + { + // Simulate BOOST_AGENTS=claudecode + Prompt::fake([ + Key::ENTER, // Accept pre-selected agents + ]); + + // This simulates the agent selection multiselect + $agentOptions = [ + 'Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode' => 'Claude Code', + 'Laravel\\Boost\\Install\\CodeEnvironment\\Copilot' => 'GitHub Copilot', + ]; + + $result = \Laravel\Prompts\multiselect( + label: 'Which agents need AI guidelines?', + options: $agentOptions, + default: ['Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode'], // Pre-selected via env + scroll: 4, + required: false, + hint: 'Pre-selected from environment variable' + ); + + $this->assertContains('Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode', $result); + $this->assertNotContains('Laravel\\Boost\\Install\\CodeEnvironment\\Copilot', $result); + } + + /** + * Test that false agent selection results in empty selection. + */ + public function test_false_agent_selection_allows_empty_result(): void + { + // Simulate BOOST_AGENTS=false + Prompt::fake([ + Key::ENTER, // Accept empty selection + ]); + + $agentOptions = [ + 'Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode' => 'Claude Code', + 'Laravel\\Boost\\Install\\CodeEnvironment\\Copilot' => 'GitHub Copilot', + ]; + + $result = \Laravel\Prompts\multiselect( + label: 'Which agents need AI guidelines?', + options: $agentOptions, + default: [], // No pre-selection (BOOST_AGENTS=false) + scroll: 4, + required: false, + hint: 'None selected via environment variable' + ); + + $this->assertEmpty($result); + } + + /** + * Test test enforcement selection with environment defaults. + */ + public function test_test_enforcement_respects_environment_default(): void + { + // Simulate BOOST_ENFORCE_TESTS=false setting default to "No" + Prompt::fake([ + Key::ENTER, // Accept default (No) + ]); + + $result = \Laravel\Prompts\select( + label: 'Should AI always create tests?', + options: ['Yes', 'No'], + default: 'No' // Set by BOOST_ENFORCE_TESTS=false + ); + + $this->assertEquals('No', $result); + } + + /** + * Test test enforcement allows user override of environment default. + */ + public function test_test_enforcement_allows_user_override(): void + { + // Even though env says false, user can choose Yes + Prompt::fake([ + Key::UP, // Move to Yes + Key::ENTER, // Select Yes + ]); + + $result = \Laravel\Prompts\select( + label: 'Should AI always create tests?', + options: ['Yes', 'No'], + default: 'No' // Default from BOOST_ENFORCE_TESTS=false + ); + + $this->assertEquals('Yes', $result); + } + + /** + * Test multiple agents selection from environment variable. + */ + public function test_multiple_agents_from_environment(): void + { + // Simulate BOOST_AGENTS=claudecode,copilot + Prompt::fake([ + Key::ENTER, // Accept pre-selected agents + ]); + + $agentOptions = [ + 'Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode' => 'Claude Code', + 'Laravel\\Boost\\Install\\CodeEnvironment\\Copilot' => 'GitHub Copilot', + ]; + + $result = \Laravel\Prompts\multiselect( + label: 'Which agents need AI guidelines?', + options: $agentOptions, + default: [ + 'Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode', + 'Laravel\\Boost\\Install\\CodeEnvironment\\Copilot', + ], // Both pre-selected + scroll: 4, + required: false, + hint: 'Pre-selected from environment variable' + ); + + $this->assertContains('Laravel\\Boost\\Install\\CodeEnvironment\\ClaudeCode', $result); + $this->assertContains('Laravel\\Boost\\Install\\CodeEnvironment\\Copilot', $result); + $this->assertCount(2, $result); + } + + /** + * Test the hierarchy: AI_GUIDELINES controls whether agents are relevant. + */ + public function test_config_hierarchy_ai_guidelines_controls_agents(): void + { + // When ai_guidelines is false, agents setting should be irrelevant + Config::set('boost.install.ai_guidelines', false); + Config::set('boost.install.mcp_server', true); + + Prompt::fake([ + Key::ENTER, // Accept default selection + ]); + + // Feature selection should only include MCP server + $installOptions = [ + 'mcp_server' => 'Boost MCP Server (with 15+ tools)', + ]; + + $result = \Laravel\Prompts\multiselect( + label: 'What do you want to install?', + options: $installOptions, + default: ['mcp_server'], + required: true, + ); + + // Only MCP server should be selected + $this->assertContains('mcp_server', $result); + $this->assertCount(1, $result); + } +}

AltStyle によって変換されたページ (->オリジナル) /