From 3edec792c1946e8f806c3faf9a4a4228ee803965 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 25 Sep 2025 22:26:12 +0100 Subject: [PATCH 01/21] Allows package authors to publish guidelines --- .ai/foundation.blade.php | 2 +- .ai/inertia-react/2/forms.blade.php | 3 +- .ai/inertia-svelte/2/forms.blade.php | 3 +- .ai/inertia-vue/2/forms.blade.php | 3 +- .ai/laravel/core.blade.php | 2 +- .ai/livewire/core.blade.php | 2 +- .ai/phpunit/core.blade.php | 2 +- .ai/pint/core.blade.php | 2 +- .ai/tailwindcss/core.blade.php | 2 +- src/Console/InstallCommand.php | 70 ++++++++++++++++++++---- src/Install/GuidelineComposer.php | 58 ++++++++++++++++++-- src/Install/GuidelineConfig.php | 5 ++ src/Mcp/Methods/CallToolWithExecutor.php | 2 +- src/Support/Composer.php | 57 +++++++++++++++++++ src/Support/Config.php | 68 +++++++++++++++++++++++ 15 files changed, 254 insertions(+), 27 deletions(-) create mode 100644 src/Support/Composer.php create mode 100644 src/Support/Config.php diff --git a/.ai/foundation.blade.php b/.ai/foundation.blade.php index 8dbc49ba..a6f7a71d 100644 --- a/.ai/foundation.blade.php +++ b/.ai/foundation.blade.php @@ -1,4 +1,4 @@ -# Laravel Boost Guidelines +# Foundation (Recommended) The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. diff --git a/.ai/inertia-react/2/forms.blade.php b/.ai/inertia-react/2/forms.blade.php index ac9920f5..941707fb 100644 --- a/.ai/inertia-react/2/forms.blade.php +++ b/.ai/inertia-react/2/forms.blade.php @@ -1,7 +1,8 @@ +## Inertia + React Forms + @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp -## Inertia + React Forms @if($assist->inertia()->hasFormComponent()) @boostsnippet("`
` Component Example", "react") diff --git a/.ai/inertia-svelte/2/forms.blade.php b/.ai/inertia-svelte/2/forms.blade.php index 2c0e2074..518f7dce 100644 --- a/.ai/inertia-svelte/2/forms.blade.php +++ b/.ai/inertia-svelte/2/forms.blade.php @@ -1,7 +1,8 @@ +## Inertia + Svelte Forms + @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp -## Inertia + Svelte Forms - There are critical differences between Svelte 4 and 5, use the `search-docs` tool for up-to-date guidance. diff --git a/.ai/inertia-vue/2/forms.blade.php b/.ai/inertia-vue/2/forms.blade.php index 9aa29627..56bc041a 100644 --- a/.ai/inertia-vue/2/forms.blade.php +++ b/.ai/inertia-vue/2/forms.blade.php @@ -1,7 +1,8 @@ +## Inertia + Vue Forms + @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp -## Inertia + Vue Forms @if($assist->inertia()->hasFormComponent()) @boostsnippet("`` Component Example", "vue") diff --git a/.ai/laravel/core.blade.php b/.ai/laravel/core.blade.php index 6c27ba6b..4fd16344 100644 --- a/.ai/laravel/core.blade.php +++ b/.ai/laravel/core.blade.php @@ -1,4 +1,4 @@ -## Do Things the Laravel Way +## Laravel - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. - If you're creating a generic PHP class, use `artisan make:class`. diff --git a/.ai/livewire/core.blade.php b/.ai/livewire/core.blade.php index 9d7e532e..1152d327 100644 --- a/.ai/livewire/core.blade.php +++ b/.ai/livewire/core.blade.php @@ -1,4 +1,4 @@ -## Livewire Core +## Livewire - Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. - Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components - State should live on the server, with the UI reflecting it. diff --git a/.ai/phpunit/core.blade.php b/.ai/phpunit/core.blade.php index 946ae9f2..703a678e 100644 --- a/.ai/phpunit/core.blade.php +++ b/.ai/phpunit/core.blade.php @@ -1,4 +1,4 @@ -## PHPUnit Core +## PHPUnit - This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit ` to create a new test. - If you see a test using "Pest", convert it to PHPUnit. diff --git a/.ai/pint/core.blade.php b/.ai/pint/core.blade.php index 0283e0ac..71ff48c1 100644 --- a/.ai/pint/core.blade.php +++ b/.ai/pint/core.blade.php @@ -1,4 +1,4 @@ -## Laravel Pint Code Formatter +## Laravel Pint - You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. - Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. diff --git a/.ai/tailwindcss/core.blade.php b/.ai/tailwindcss/core.blade.php index 522c4a60..46bcc237 100644 --- a/.ai/tailwindcss/core.blade.php +++ b/.ai/tailwindcss/core.blade.php @@ -1,4 +1,4 @@ -## Tailwind Core +## Tailwind - Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. - Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 4a621772..540e05f2 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -19,6 +19,7 @@ use Laravel\Boost\Install\GuidelineConfig; use Laravel\Boost\Install\GuidelineWriter; use Laravel\Boost\Install\Herd; +use Laravel\Boost\Support\Config; use Laravel\Prompts\Concerns\Colors; use Laravel\Prompts\Terminal; use Symfony\Component\Console\Attribute\AsCommand; @@ -50,6 +51,9 @@ class InstallCommand extends Command /** @var Collection */ private Collection $selectedBoostFeatures; + /** @var Collection */ + private Collection $selectedAiGuidelines; + private string $projectName; /** @var array */ @@ -65,6 +69,11 @@ class InstallCommand extends Command private string $redCross; + public function __construct(protected Config $config) + { + parent::__construct(); + } + public function handle(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, Terminal $terminal): void { $this->bootstrap($codeEnvironmentsDetector, $herd, $terminal); @@ -122,6 +131,7 @@ protected function discoverEnvironment(): void protected function collectInstallationPreferences(): void { $this->selectedBoostFeatures = $this->selectBoostFeatures(); + $this->selectedAiGuidelines = $this->selectAiGuidelines(); $this->selectedTargetMcpClient = $this->selectTargetMcpClients(); $this->selectedTargetAgents = $this->selectTargetAgents(); $this->enforceTests = $this->determineTestEnforcement(ask: false); @@ -232,7 +242,11 @@ protected function determineTestEnforcement(bool $ask = true): bool */ protected function selectBoostFeatures(): Collection { - $defaultInstallOptions = ['mcp_server', 'ai_guidelines']; + $defaultInstallOptions = [ + 'mcp_server', + ...$this->config->exists() === false || $this->config->getAiGuidelines() !== [] ? ['ai_guidelines'] : [], + ]; + $installOptions = [ 'mcp_server' => 'Boost MCP Server (with 15+ tools)', 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', @@ -240,16 +254,45 @@ protected function selectBoostFeatures(): Collection if ($this->herd->isMcpAvailable()) { $installOptions['herd_mcp'] = 'Herd MCP Server'; + } - return collect(multiselect( - label: 'What do you want to install?', - options: $installOptions, - default: $defaultInstallOptions, - required: true, - )); + return collect(multiselect( + label: 'What do you want to install?', + options: $installOptions, + default: $defaultInstallOptions, + required: true, + )); + } + + /** + * @return Collection + */ + protected function selectAiGuidelines(): Collection + { + if (! $this->shouldInstallAiGuidelines()) { + return collect(); + } + + $aiGuidelines = collect($this->config->getAiGuidelines()); + + $options = app(GuidelineComposer::class)->guidelines(); + $defaults = $aiGuidelines->isNotEmpty() + ? $aiGuidelines + : $options->reject(fn (array $guideline) => $guideline['third_party'])->keys(); + + if ($options->isEmpty()) { + return collect(); } - return collect(['mcp_server', 'ai_guidelines']); + return collect(multiselect( + label: 'Which AI guidelines do you want to install?', + // @phpstan-ignore-next-line + options: $options->mapWithKeys(fn (array $guideline, string $name) => [$name => "{$name} (~{$guideline['tokens']} tokens) {$guideline['description']}"]), + default: $defaults, + scroll: 10, + hint: 'You can add or remove them later by running this command again', + required: true, + )); } /** @@ -265,10 +308,6 @@ protected function boostToolsToDisable(): array ); } - /** - * @return array - */ - /** * @return Collection */ @@ -369,6 +408,8 @@ protected function selectCodeEnvironments(string $contractClass, string $label): protected function installGuidelines(): void { if (! $this->shouldInstallAiGuidelines()) { + $this->config->setAiGuidelines([]); + return; } @@ -383,6 +424,7 @@ protected function installGuidelines(): void $guidelineConfig->laravelStyle = $this->shouldInstallStyleGuidelines(); $guidelineConfig->caresAboutLocalization = $this->detectLocalization(); $guidelineConfig->hasAnApi = false; + $guidelineConfig->aiGuidelines = $this->selectedAiGuidelines->values()->toArray(); $composer = app(GuidelineComposer::class)->config($guidelineConfig); $guidelines = $composer->guidelines(); @@ -432,6 +474,10 @@ protected function installGuidelines(): void $this->line(" - {$agentName}: {$error}"); } } + + $this->config->setAiGuidelines( + $this->selectedAiGuidelines->values()->toArray() + ); } protected function shouldInstallAiGuidelines(): bool diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 826611e5..1ee25ce3 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -6,11 +6,14 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Str; +use Laravel\Boost\Support\Composer; use Laravel\Roster\Enums\Packages; use Laravel\Roster\Package; use Laravel\Roster\Roster; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; class GuidelineComposer { @@ -177,7 +180,37 @@ protected function find(): Collection $guidelines->put('.ai/'.$guideline['name'], $guideline); } + $pathsUsed = $guidelines->pluck('path'); + + foreach ($userGuidelines as $guideline) { + if ($pathsUsed->contains($guideline['path'])) { + continue; // Don't include this twice if it's an override + } + + $guidelines->put('.ai/'.$guideline['name'], $guideline); + } + + collect(Composer::packagesDirectoriesWithBoostGuidelines()) + ->each(function (string $path, string $package) use ($guidelines): void { + $packageGuidelines = $this->guidelinesDir($path, true); + $pathsUsed = $guidelines->pluck('path'); + + foreach ($packageGuidelines as $guideline) { + if ($pathsUsed->contains($guideline['path'])) { + continue; // Don't include this twice if it's an override + } + + $guidelines->put($package, $guideline); + } + }); + return $guidelines + ->when( + isset($this->config->aiGuidelines), + fn (Collection $collection): Collection => $collection->filter( + fn (array $guideline, string $key): bool => in_array($key, $this->config->aiGuidelines, true), + ) + ) ->where(fn (array $guideline): bool => ! empty(trim((string) $guideline['content']))); } @@ -201,7 +234,7 @@ protected function shouldExcludePackage(Package $package): bool /** * @return array */ - protected function guidelinesDir(string $dirPath): array + protected function guidelinesDir(string $dirPath, bool $thirdParty = false): array { if (! is_dir($dirPath)) { $dirPath = str_replace('/', DIRECTORY_SEPARATOR, __DIR__.'/../../.ai/'.$dirPath); @@ -216,17 +249,24 @@ protected function guidelinesDir(string $dirPath): array return []; } - return array_map(fn (\Symfony\Component\Finder\SplFileInfo $file): array => $this->guideline($file->getRealPath()), iterator_to_array($finder)); + return array_map(fn (SplFileInfo $file): array => $this->guideline($file->getRealPath(), $thirdParty), iterator_to_array($finder)); } /** - * @return array{content: string, name: string, path: ?string, custom: bool} + * @return array{content: string, name: string, description: string, path: ?string, custom: bool, third_party: bool} */ - protected function guideline(string $path): array + protected function guideline(string $path, bool $thirdParty = false): array { $path = $this->guidelinePath($path); if (is_null($path)) { - return ['content' => '', 'name' => '', 'path' => null, 'custom' => false]; + return [ + 'content' => '', + 'description' => '', + 'name' => '', + 'path' => null, + 'custom' => false, + 'third_party' => $thirdParty, + ]; } $content = file_get_contents($path); @@ -248,11 +288,19 @@ protected function guideline(string $path): array $this->storedSnippets = []; // Clear for next use + $description = Str::of($rendered)->after('# ')->before("\n")->trim() + ->whenEmpty(fn () => 'No description provided') + ->limit(50, '...') + ->value(); + return [ 'content' => trim($rendered), 'name' => str_replace('.blade.php', '', basename($path)), + 'description' => $description, 'path' => $path, 'custom' => str_contains($path, $this->customGuidelinePath()), + 'third_party' => $thirdParty, + 'tokens' => str_word_count($rendered) * 1.3, // Rough estimate of tokens based on word count ]; } diff --git a/src/Install/GuidelineConfig.php b/src/Install/GuidelineConfig.php index 90cd166d..20baccbf 100644 --- a/src/Install/GuidelineConfig.php +++ b/src/Install/GuidelineConfig.php @@ -13,4 +13,9 @@ class GuidelineConfig public bool $caresAboutLocalization = false; public bool $hasAnApi = false; + + /** + * @var array + */ + public array $aiGuidelines; } diff --git a/src/Mcp/Methods/CallToolWithExecutor.php b/src/Mcp/Methods/CallToolWithExecutor.php index 9db398a5..51b6a858 100644 --- a/src/Mcp/Methods/CallToolWithExecutor.php +++ b/src/Mcp/Methods/CallToolWithExecutor.php @@ -38,7 +38,7 @@ public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpc } $tool = $context - ->tools($request->toRequest()) + ->tools() ->first( fn ($tool): bool => $tool->name() === $request->params['name'], fn () => throw new JsonRpcException( diff --git a/src/Support/Composer.php b/src/Support/Composer.php new file mode 100644 index 00000000..d2d1e541 --- /dev/null +++ b/src/Support/Composer.php @@ -0,0 +1,57 @@ +mapWithKeys(fn (string $key, string $package) => [$package => implode(DIRECTORY_SEPARATOR, [ + base_path('vendor'), + str_replace('/', DIRECTORY_SEPARATOR, $package), + ])]) + ->filter(fn (string $path) => is_dir($path)) + ->toArray(); + } + + public static function packages(): array + { + $composerJsonPath = base_path('composer.json'); + + if (! file_exists($composerJsonPath)) { + return []; + } + + $composerData = json_decode(file_get_contents($composerJsonPath), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return []; + } + + return collect($composerData['require'] ?? []) + ->merge($composerData['require-dev'] ?? []) + ->mapWithKeys(fn (string $key, string $package) => [$package => $key]) + ->toArray(); + } + + /** + * Get all packages directories that contain Boost guidelines. + */ + public static function packagesDirectoriesWithBoostGuidelines(): array + { + return collect(Composer::packagesDirectories()) + ->map(fn (string $path) => implode(DIRECTORY_SEPARATOR, [ + $path, + 'resources', + 'boost', + 'guidelines', + ]))->filter(fn (string $path) => is_dir($path)) + ->toArray(); + } +} diff --git a/src/Support/Config.php b/src/Support/Config.php new file mode 100644 index 00000000..c5aed34e --- /dev/null +++ b/src/Support/Config.php @@ -0,0 +1,68 @@ + + */ + public function getAiGuidelines(): array + { + return $this->get('guidelines', []); + } + + /** + * @param array $guidelines + */ + public function setAiGuidelines(array $guidelines): void + { + $this->set('guidelines', $guidelines); + } + + protected function get(string $key, mixed $default = null): mixed + { + $config = $this->all(); + + return data_get($config, $key, $default); + } + + protected function set(string $key, mixed $value): void + { + $config = array_filter($this->all()); + + data_set($config, $key, $value); + + $path = base_path(self::FILE); + + file_put_contents($path, Str::of(json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))->append(PHP_EOL)); + } + + protected function all(): array + { + $path = base_path(self::FILE); + + if (! file_exists($path)) { + return []; + } + + $config = json_decode(file_get_contents($path), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return []; + } + + return $config ?? []; + } +} From 387ea621372a7e9322b5e3314c6a15fcd385103e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 25 Sep 2025 22:52:26 +0100 Subject: [PATCH 02/21] wip --- src/Console/InstallCommand.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 540e05f2..40b1f591 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -287,7 +287,11 @@ protected function selectAiGuidelines(): Collection return collect(multiselect( label: 'Which AI guidelines do you want to install?', // @phpstan-ignore-next-line - options: $options->mapWithKeys(fn (array $guideline, string $name) => [$name => "{$name} (~{$guideline['tokens']} tokens) {$guideline['description']}"]), + options: $options->mapWithKeys(function (array $guideline, string $name) { + $humanName = str_replace('/core', '', $name); + + return [$name => "{$humanName} (~{$guideline['tokens']} tokens) {$guideline['description']}"]; + }), default: $defaults, scroll: 10, hint: 'You can add or remove them later by running this command again', From 21718bcbf4937c5676122ef7773de22ba8087065 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 16:41:55 +0100 Subject: [PATCH 03/21] Adjusts things --- src/Console/InstallCommand.php | 205 +++++++----------- src/Contracts/Agent.php | 2 + src/Contracts/McpClient.php | 2 + src/Install/CodeEnvironment/ClaudeCode.php | 2 +- .../CodeEnvironment/CodeEnvironment.php | 22 ++ src/Install/GuidelineComposer.php | 11 +- src/Support/Config.php | 47 +++- .../Feature/Console/InstallCommandWslTest.php | 11 +- .../Install/CodeEnvironmentsDetectorTest.php | 6 +- 9 files changed, 161 insertions(+), 147 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 40b1f591..78dc022f 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -26,10 +26,10 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Process; +use function Laravel\Prompts\confirm; use function Laravel\Prompts\intro; use function Laravel\Prompts\multiselect; use function Laravel\Prompts\note; -use function Laravel\Prompts\select; #[AsCommand('boost:install', 'Install Laravel Boost')] class InstallCommand extends Command @@ -134,7 +134,7 @@ protected function collectInstallationPreferences(): void $this->selectedAiGuidelines = $this->selectAiGuidelines(); $this->selectedTargetMcpClient = $this->selectTargetMcpClients(); $this->selectedTargetAgents = $this->selectTargetAgents(); - $this->enforceTests = $this->determineTestEnforcement(ask: false); + $this->enforceTests = $this->determineTestEnforcement(); } protected function performInstallation(): void @@ -143,7 +143,7 @@ protected function performInstallation(): void usleep(750000); - if (($this->shouldInstallMcp() || $this->shouldInstallHerdMcp()) && $this->selectedTargetMcpClient->isNotEmpty()) { + if ($this->selectedTargetMcpClient->isNotEmpty()) { $this->installMcpServerConfig(); } } @@ -179,9 +179,8 @@ protected function outro(): void $boostFeatures = $this->selectedBoostFeatures->map(fn ($feature): string => 'b:'.$feature)->toArray(); $guidelines = []; - if ($this->shouldInstallAiGuidelines()) { - $guidelines[] = 'g:ai'; - } + + $guidelines[] = 'g:ai'; if ($this->shouldInstallStyleGuidelines()) { $guidelines[] = 'g:style'; @@ -210,7 +209,7 @@ protected function hyperlink(string $label, string $url): string * won't have the CI setup to make use of them anyway, so we're just wasting their * tokens/money by enforcing them. */ - protected function determineTestEnforcement(bool $ask = true): bool + protected function determineTestEnforcement(): bool { $hasMinimumTests = false; @@ -226,14 +225,6 @@ protected function determineTestEnforcement(bool $ask = true): bool ->count() >= self::MIN_TEST_COUNT; } - if (! $hasMinimumTests && $ask) { - return select( - label: 'Should AI always create tests?', - options: ['Yes', 'No'], - default: 'Yes' - ) === 'Yes'; - } - return $hasMinimumTests; } @@ -242,26 +233,24 @@ protected function determineTestEnforcement(bool $ask = true): bool */ protected function selectBoostFeatures(): Collection { - $defaultInstallOptions = [ + $features = collect([ 'mcp_server', - ...$this->config->exists() === false || $this->config->getAiGuidelines() !== [] ? ['ai_guidelines'] : [], - ]; + 'ai_guidelines', + ]); - $installOptions = [ - 'mcp_server' => 'Boost MCP Server (with 15+ tools)', - 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', - ]; + if ($this->herd->isMcpAvailable() === false) { + return $features; + } - if ($this->herd->isMcpAvailable()) { - $installOptions['herd_mcp'] = 'Herd MCP Server'; + if (confirm( + label: 'Would you like to install Herd MCP alongside Boost MCP?', + default: $this->config->getHerdMcp(), + hint: 'Herd MCP provides additional tools like browser logs, that can help AI understand issues better', + )) { + $features->push('herd_mcp'); } - return collect(multiselect( - label: 'What do you want to install?', - options: $installOptions, - default: $defaultInstallOptions, - required: true, - )); + return $features; } /** @@ -269,61 +258,36 @@ protected function selectBoostFeatures(): Collection */ protected function selectAiGuidelines(): Collection { - if (! $this->shouldInstallAiGuidelines()) { - return collect(); - } - - $aiGuidelines = collect($this->config->getAiGuidelines()); - - $options = app(GuidelineComposer::class)->guidelines(); - $defaults = $aiGuidelines->isNotEmpty() - ? $aiGuidelines - : $options->reject(fn (array $guideline) => $guideline['third_party'])->keys(); + $options = app(GuidelineComposer::class)->guidelines() + ->reject(fn (array $guideline) => $guideline['third_party'] === false); if ($options->isEmpty()) { return collect(); } return collect(multiselect( - label: 'Which AI guidelines do you want to install?', + label: 'Which Third Party AI Guidelines do you want to install?', // @phpstan-ignore-next-line options: $options->mapWithKeys(function (array $guideline, string $name) { $humanName = str_replace('/core', '', $name); return [$name => "{$humanName} (~{$guideline['tokens']} tokens) {$guideline['description']}"]; }), - default: $defaults, + default: collect($this->config->getGuidelines()), scroll: 10, hint: 'You can add or remove them later by running this command again', - required: true, )); } - /** - * @return array - */ - protected function boostToolsToDisable(): array - { - return multiselect( - label: 'Do you need to disable any Boost provided tools?', - options: $this->discoverTools(), - scroll: 4, - hint: 'You can exclude or include them later in the config file', - ); - } - /** * @return Collection */ protected function selectTargetMcpClients(): Collection { - if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { - return collect(); - } - return $this->selectCodeEnvironments( McpClient::class, - sprintf('Which code editors do you use to work on %s?', $this->projectName) + sprintf('Which code editors do you use to work on %s?', $this->projectName), + $this->config->getEditors(), ); } @@ -332,13 +296,10 @@ protected function selectTargetMcpClients(): Collection */ protected function selectTargetAgents(): Collection { - if (! $this->shouldInstallAiGuidelines()) { - return collect(); - } - return $this->selectCodeEnvironments( Agent::class, - sprintf('Which agents need AI guidelines for %s?', $this->projectName) + sprintf('Which agents need AI guidelines for %s?', $this->projectName), + $this->config->getAgents(), ); } @@ -357,9 +318,10 @@ protected function getSelectionConfig(string $contractClass): array } /** + * @param array $defaults * @return Collection */ - protected function selectCodeEnvironments(string $contractClass, string $label): Collection + protected function selectCodeEnvironments(string $contractClass, string $label, array $defaults): Collection { $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); $config = $this->getSelectionConfig($contractClass); @@ -374,49 +336,48 @@ protected function selectCodeEnvironments(string $contractClass, string $label): $displayMethod = $config['displayMethod']; $displayText = $environment->{$displayMethod}(); - return [$environment::class => $displayText]; + return [$environment->name() => $displayText]; })->sort(); - $detectedClasses = []; $installedEnvNames = array_unique(array_merge( $this->projectInstalledCodeEnvironments, $this->systemInstalledCodeEnvironments )); - foreach ($installedEnvNames as $envKey) { - $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env): bool => strtolower((string) $envKey) === strtolower($env->name())); - if ($matchingEnv) { - $detectedClasses[] = $matchingEnv::class; + $detectedDefaults = []; + + if ($defaults === []) { + foreach ($installedEnvNames as $envKey) { + $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env): bool => strtolower((string) $envKey) === strtolower($env->name())); + if ($matchingEnv) { + $detectedDefaults[] = $matchingEnv->name(); + } } } - $selectedClasses = collect(multiselect( + $selectedCodeEnvironments = collect(multiselect( label: $label, options: $options->toArray(), - default: array_unique($detectedClasses), + default: $defaults === [] ? $detectedDefaults : $defaults, scroll: $config['scroll'], required: $config['required'], - hint: $detectedClasses === [] ? '' : sprintf('Auto-detected %s for you', + hint: $defaults === [] || $detectedDefaults === [] ? '' : sprintf('Auto-detected %s for you', Arr::join(array_map(function ($className) use ($availableEnvironments, $config) { - $env = $availableEnvironments->first(fn ($env): bool => $env::class === $className); + $env = $availableEnvironments->first(fn ($env): bool => $env->name() === $className); $displayMethod = $config['displayMethod']; return $env->{$displayMethod}(); - }, $detectedClasses), ', ', ' & ') + }, $detectedDefaults), ', ', ' & ') ) ))->sort(); - return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env): bool => $env::class === $className)); + return $selectedCodeEnvironments->map( + fn (string $name) => $availableEnvironments->first(fn ($env): bool => $env->name() === $name), + )->filter()->values(); } protected function installGuidelines(): void { - if (! $this->shouldInstallAiGuidelines()) { - $this->config->setAiGuidelines([]); - - return; - } - if ($this->selectedTargetAgents->isEmpty()) { $this->info(' No agents selected for guideline installation.'); @@ -479,14 +440,21 @@ protected function installGuidelines(): void } } - $this->config->setAiGuidelines( - $this->selectedAiGuidelines->values()->toArray() + $this->config->setHerdMcp( + $this->shouldInstallHerdMcp() ); - } - protected function shouldInstallAiGuidelines(): bool - { - return $this->selectedBoostFeatures->contains('ai_guidelines'); + $this->config->setEditors( + $this->selectedTargetMcpClient->map(fn (McpClient $mcpClient) => $mcpClient->name())->values()->toArray() + ); + + $this->config->setAgents( + $this->selectedTargetAgents->map(fn (Agent $agent) => $agent->name())->values()->toArray() + ); + + $this->config->setGuidelines( + $this->selectedAiGuidelines->values()->toArray() + ); } protected function shouldInstallStyleGuidelines(): bool @@ -494,11 +462,6 @@ protected function shouldInstallStyleGuidelines(): bool return false; } - protected function shouldInstallMcp(): bool - { - return $this->selectedBoostFeatures->contains('mcp_server'); - } - protected function shouldInstallHerdMcp(): bool { return $this->selectedBoostFeatures->contains('herd_mcp'); @@ -506,10 +469,6 @@ protected function shouldInstallHerdMcp(): bool protected function installMcpServerConfig(): void { - if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { - return; - } - if ($this->selectedTargetMcpClient->isEmpty()) { $this->info('No agents selected for guideline installation.'); @@ -537,32 +496,30 @@ protected function installMcpServerConfig(): void $this->output->write(" {$ideDisplay}... "); $results = []; - if ($this->shouldInstallMcp()) { - $inWsl = $this->isRunningInWsl(); - $mcp = array_filter([ - 'laravel-boost', - $inWsl ? 'wsl' : false, - $mcpClient->getPhpPath($inWsl), - $mcpClient->getArtisanPath($inWsl), - 'boost:mcp', - ]); - try { - $result = $mcpClient->installMcp( - array_shift($mcp), - array_shift($mcp), - $mcp - ); - - if ($result) { - $results[] = $this->greenTick.' Boost'; - } else { - $results[] = $this->redCross.' Boost'; - $failed[$ideName]['boost'] = 'Failed to write configuration'; - } - } catch (Exception $e) { + $inWsl = $this->isRunningInWsl(); + $mcp = array_filter([ + 'laravel-boost', + $inWsl ? 'wsl' : false, + $mcpClient->getPhpPath($inWsl), + $mcpClient->getArtisanPath($inWsl), + 'boost:mcp', + ]); + try { + $result = $mcpClient->installMcp( + array_shift($mcp), + array_shift($mcp), + $mcp + ); + + if ($result) { + $results[] = $this->greenTick.' Boost'; + } else { $results[] = $this->redCross.' Boost'; - $failed[$ideName]['boost'] = $e->getMessage(); + $failed[$ideName]['boost'] = 'Failed to write configuration'; } + } catch (Exception $e) { + $results[] = $this->redCross.' Boost'; + $failed[$ideName]['boost'] = $e->getMessage(); } // Install Herd MCP if enabled diff --git a/src/Contracts/Agent.php b/src/Contracts/Agent.php index f6bbf793..da3f7b8f 100644 --- a/src/Contracts/Agent.php +++ b/src/Contracts/Agent.php @@ -9,6 +9,8 @@ */ interface Agent { + public function name(): string; + /** * Get the display name of the Agent. */ diff --git a/src/Contracts/McpClient.php b/src/Contracts/McpClient.php index 53fb2929..89c11001 100644 --- a/src/Contracts/McpClient.php +++ b/src/Contracts/McpClient.php @@ -9,6 +9,8 @@ */ interface McpClient { + public function name(): string; + /** * Get the display name of the MCP (Model Context Protocol) client. */ diff --git a/src/Install/CodeEnvironment/ClaudeCode.php b/src/Install/CodeEnvironment/ClaudeCode.php index 7babc622..83687f2e 100644 --- a/src/Install/CodeEnvironment/ClaudeCode.php +++ b/src/Install/CodeEnvironment/ClaudeCode.php @@ -13,7 +13,7 @@ class ClaudeCode extends CodeEnvironment implements Agent, McpClient { public function name(): string { - return 'claudecode'; + return 'claude_code'; } public function displayName(): string diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 9a96b052..0dcdba30 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -93,6 +93,28 @@ public function mcpInstallationStrategy(): McpInstallationStrategy return McpInstallationStrategy::FILE; } + final public static function fromName(string $name): ?static + { + $detectionFactory = app(DetectionStrategyFactory::class); + + foreach ([ + ClaudeCode::class, + Codex::class, + Copilot::class, + Cursor::class, + PhpStorm::class, + VSCode::class, + ] as $class) { + /** @var class-string $class */ + $instance = new $class($detectionFactory); + if ($instance->name() === $name) { + return $instance; + } + } + + return null; + } + public function shellMcpCommand(): ?string { return null; diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 1ee25ce3..7bb410ad 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -202,15 +202,14 @@ protected function find(): Collection $guidelines->put($package, $guideline); } - }); - - return $guidelines - ->when( + })->when( isset($this->config->aiGuidelines), fn (Collection $collection): Collection => $collection->filter( - fn (array $guideline, string $key): bool => in_array($key, $this->config->aiGuidelines, true), + fn (string $name): bool => in_array($name, $this->config->aiGuidelines, true), ) - ) + ); + + return $guidelines ->where(fn (array $guideline): bool => ! empty(trim((string) $guideline['content']))); } diff --git a/src/Support/Config.php b/src/Support/Config.php index c5aed34e..d7a6bfd3 100644 --- a/src/Support/Config.php +++ b/src/Support/Config.php @@ -10,15 +10,10 @@ class Config { protected const FILE = 'boost.json'; - public function exists(): bool - { - return file_exists(base_path(self::FILE)); - } - /** * @return array */ - public function getAiGuidelines(): array + public function getGuidelines(): array { return $this->get('guidelines', []); } @@ -26,11 +21,47 @@ public function getAiGuidelines(): array /** * @param array $guidelines */ - public function setAiGuidelines(array $guidelines): void + public function setGuidelines(array $guidelines): void { $this->set('guidelines', $guidelines); } + public function setEditors(array $editors): void + { + $this->set('editors', $editors); + } + + /** + * @param array $agents + */ + public function setAgents(array $agents): void + { + $this->set('agents', $agents); + } + + /** + * @return array + */ + public function getAgents(): array + { + return $this->get('agents', []); + } + + public function getEditors(): array + { + return $this->get('editors', []); + } + + public function setHerdMcp(bool $installed): void + { + $this->set('herd_mcp', $installed); + } + + public function getHerdMcp(): bool + { + return $this->get('herd_mcp', false); + } + protected function get(string $key, mixed $default = null): mixed { $config = $this->all(); @@ -40,7 +71,7 @@ protected function get(string $key, mixed $default = null): mixed protected function set(string $key, mixed $value): void { - $config = array_filter($this->all()); + $config = $this->all(); data_set($config, $key, $value); diff --git a/tests/Feature/Console/InstallCommandWslTest.php b/tests/Feature/Console/InstallCommandWslTest.php index 1367df15..442e9d38 100644 --- a/tests/Feature/Console/InstallCommandWslTest.php +++ b/tests/Feature/Console/InstallCommandWslTest.php @@ -3,11 +3,12 @@ declare(strict_types=1); use Laravel\Boost\Console\InstallCommand; +use Laravel\Boost\Support\Config; test('isRunningInWsl returns true when WSL_DISTRO_NAME is set', function (): void { putenv('WSL_DISTRO_NAME=Ubuntu'); - $command = new InstallCommand; + $command = new InstallCommand(new Config()); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -17,7 +18,7 @@ test('isRunningInWsl returns true when IS_WSL is set', function (): void { putenv('IS_WSL=1'); - $command = new InstallCommand; + $command = new InstallCommand(new Config()); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -28,7 +29,7 @@ putenv('WSL_DISTRO_NAME=Ubuntu'); putenv('IS_WSL=true'); - $command = new InstallCommand; + $command = new InstallCommand(new Config()); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -39,7 +40,7 @@ putenv('WSL_DISTRO_NAME'); putenv('IS_WSL'); - $command = new InstallCommand; + $command = new InstallCommand(new Config()); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -50,7 +51,7 @@ putenv('WSL_DISTRO_NAME='); putenv('IS_WSL='); - $command = new InstallCommand; + $command = new InstallCommand(new Config()); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); diff --git a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php index dc46503c..69c60b5f 100644 --- a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php +++ b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php @@ -139,7 +139,7 @@ $detected = $this->detector->discoverProjectInstalledCodeEnvironments($tempDir); - expect($detected)->toContain('claudecode'); + expect($detected)->toContain('claude_code'); unlink($tempDir.'/CLAUDE.md'); rmdir($tempDir); @@ -168,7 +168,7 @@ $detected = $this->detector->discoverProjectInstalledCodeEnvironments($tempDir); - expect($detected)->toContain('claudecode'); + expect($detected)->toContain('claude_code'); rmdir($tempDir.'/.claude'); rmdir($tempDir); @@ -253,7 +253,7 @@ expect($detected)->toContain('vscode') ->and($detected)->toContain('cursor') - ->and($detected)->toContain('claudecode') + ->and($detected)->toContain('claude_code') ->and(count($detected))->toBeGreaterThanOrEqual(3); // Cleanup From 53c24547069afd464efe3028512debc7f1dc8c18 Mon Sep 17 00:00:00 2001 From: nunomaduro <5457236+nunomaduro@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:42:19 +0000 Subject: [PATCH 04/21] Fix code styling --- tests/Feature/Console/InstallCommandWslTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Console/InstallCommandWslTest.php b/tests/Feature/Console/InstallCommandWslTest.php index 442e9d38..7b5dac1d 100644 --- a/tests/Feature/Console/InstallCommandWslTest.php +++ b/tests/Feature/Console/InstallCommandWslTest.php @@ -8,7 +8,7 @@ test('isRunningInWsl returns true when WSL_DISTRO_NAME is set', function (): void { putenv('WSL_DISTRO_NAME=Ubuntu'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -18,7 +18,7 @@ test('isRunningInWsl returns true when IS_WSL is set', function (): void { putenv('IS_WSL=1'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -29,7 +29,7 @@ putenv('WSL_DISTRO_NAME=Ubuntu'); putenv('IS_WSL=true'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -40,7 +40,7 @@ putenv('WSL_DISTRO_NAME'); putenv('IS_WSL'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -51,7 +51,7 @@ putenv('WSL_DISTRO_NAME='); putenv('IS_WSL='); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); From ece3717ec5fc74ea65d1aae406654893efc7d0ed Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:04:32 +0100 Subject: [PATCH 05/21] Finalizes --- src/Install/GuidelineWriter.php | 4 +- src/Support/Composer.php | 6 -- src/Support/Config.php | 9 +++ .../Feature/Console/InstallCommandWslTest.php | 10 +-- tests/Pest.php | 6 +- .../CodeEnvironment/CodeEnvironmentTest.php | 4 -- .../Install/CodeEnvironmentsDetectorTest.php | 4 -- .../CompositeDetectionStrategyTest.php | 4 -- tests/Unit/Install/Mcp/FileWriterTest.php | 14 ++-- tests/Unit/Support/ComposerTest.php | 66 +++++++++++++++++++ tests/Unit/Support/ConfigTest.php | 66 +++++++++++++++++++ tests/Unit/UnitTest.php | 7 -- 12 files changed, 157 insertions(+), 43 deletions(-) create mode 100644 tests/Unit/Support/ComposerTest.php create mode 100644 tests/Unit/Support/ConfigTest.php delete mode 100644 tests/Unit/UnitTest.php diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index dbe758e4..805caa68 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -31,11 +31,11 @@ public function write(string $guidelines): int $filePath = $this->agent->guidelinesPath(); $directory = dirname($filePath); - if (! is_dir($directory) && ! mkdir($directory, 0755, true)) { + if (! is_dir($directory) && ! @mkdir($directory, 0755, true)) { throw new RuntimeException("Failed to create directory: {$directory}"); } - $handle = fopen($filePath, 'c+'); + $handle = @fopen($filePath, 'c+'); if (! $handle) { throw new RuntimeException("Failed to open file: {$filePath}"); } diff --git a/src/Support/Composer.php b/src/Support/Composer.php index d2d1e541..25994027 100644 --- a/src/Support/Composer.php +++ b/src/Support/Composer.php @@ -6,9 +6,6 @@ class Composer { - /** - * Get all packages directories listed in composer.json (both require and require-dev). - */ public static function packagesDirectories(): array { return collect(static::packages()) @@ -40,9 +37,6 @@ public static function packages(): array ->toArray(); } - /** - * Get all packages directories that contain Boost guidelines. - */ public static function packagesDirectoriesWithBoostGuidelines(): array { return collect(Composer::packagesDirectories()) diff --git a/src/Support/Config.php b/src/Support/Config.php index d7a6bfd3..66ce0f2c 100644 --- a/src/Support/Config.php +++ b/src/Support/Config.php @@ -62,6 +62,15 @@ public function getHerdMcp(): bool return $this->get('herd_mcp', false); } + public function flush(): void + { + $path = base_path(self::FILE); + + if (file_exists($path)) { + unlink($path); + } + } + protected function get(string $key, mixed $default = null): mixed { $config = $this->all(); diff --git a/tests/Feature/Console/InstallCommandWslTest.php b/tests/Feature/Console/InstallCommandWslTest.php index 442e9d38..7b5dac1d 100644 --- a/tests/Feature/Console/InstallCommandWslTest.php +++ b/tests/Feature/Console/InstallCommandWslTest.php @@ -8,7 +8,7 @@ test('isRunningInWsl returns true when WSL_DISTRO_NAME is set', function (): void { putenv('WSL_DISTRO_NAME=Ubuntu'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -18,7 +18,7 @@ test('isRunningInWsl returns true when IS_WSL is set', function (): void { putenv('IS_WSL=1'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -29,7 +29,7 @@ putenv('WSL_DISTRO_NAME=Ubuntu'); putenv('IS_WSL=true'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -40,7 +40,7 @@ putenv('WSL_DISTRO_NAME'); putenv('IS_WSL'); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); @@ -51,7 +51,7 @@ putenv('WSL_DISTRO_NAME='); putenv('IS_WSL='); - $command = new InstallCommand(new Config()); + $command = new InstallCommand(new Config); $reflection = new ReflectionClass($command); $method = $reflection->getMethod('isRunningInWsl'); diff --git a/tests/Pest.php b/tests/Pest.php index 05eb243b..834f4103 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,9 +13,13 @@ | */ +use Laravel\Boost\Support\Config; use Laravel\Mcp\Response; -uses(Tests\TestCase::class)->in('Feature'); +uses(Tests\TestCase::class)->in('Unit', 'Feature') + ->beforeEach(function () { + (new Config)->flush(); + }); expect()->extend('isToolResult', fn () => $this->toBeInstanceOf(Response::class)); diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index 25c54906..4b6e08f9 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -21,10 +21,6 @@ $this->strategy = Mockery::mock(DetectionStrategy::class); }); -afterEach(function (): void { - Mockery::close(); -}); - // Create a concrete test implementation for testing abstract methods class TestCodeEnvironment extends CodeEnvironment { diff --git a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php index 69c60b5f..cf80d842 100644 --- a/tests/Unit/Install/CodeEnvironmentsDetectorTest.php +++ b/tests/Unit/Install/CodeEnvironmentsDetectorTest.php @@ -11,10 +11,6 @@ $this->detector = new CodeEnvironmentsDetector($this->container); }); -afterEach(function (): void { - Mockery::close(); -}); - test('discoverSystemInstalledCodeEnvironments returns detected programs', function (): void { // Create mock programs $program1 = Mockery::mock(CodeEnvironment::class); diff --git a/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php b/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php index a7feaab1..cb3a7dd7 100644 --- a/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php +++ b/tests/Unit/Install/Detection/CompositeDetectionStrategyTest.php @@ -12,10 +12,6 @@ $this->thirdStrategy = Mockery::mock(DetectionStrategy::class); }); -afterEach(function (): void { - Mockery::close(); -}); - test('returns true when first strategy succeeds', function (): void { $this->firstStrategy ->shouldReceive('detect') diff --git a/tests/Unit/Install/Mcp/FileWriterTest.php b/tests/Unit/Install/Mcp/FileWriterTest.php index 065eb7d7..177a1c2c 100644 --- a/tests/Unit/Install/Mcp/FileWriterTest.php +++ b/tests/Unit/Install/Mcp/FileWriterTest.php @@ -4,16 +4,13 @@ namespace Tests\Unit\Install\Mcp; +use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; use Laravel\Boost\Install\Mcp\FileWriter; use Mockery; use ReflectionClass; -beforeEach(function (): void { - Mockery::close(); -}); - test('constructor sets file path', function (): void { $writer = new FileWriter('/path/to/mcp.json'); expect($writer)->toBeInstanceOf(FileWriter::class); @@ -283,8 +280,7 @@ test("injecting twice into existing JSON 5 doesn't cause duplicates", function (): void { $capturedContent = ''; - File::clearResolvedInstances(); - File::partialMock(); + File::swap(Mockery::mock(Filesystem::class)); File::shouldReceive('ensureDirectoryExists')->once(); File::shouldReceive('exists')->andReturn(true); @@ -313,8 +309,7 @@ $newContent = $capturedContent; - File::clearResolvedInstances(); - File::partialMock(); + File::swap(Mockery::mock(Filesystem::class)); File::shouldReceive('ensureDirectoryExists')->once(); File::shouldReceive('exists')->andReturn(true); @@ -388,8 +383,7 @@ function mockFileOperations(bool $fileExists = false, string $content = '{}', bool $writeSuccess = true, ?string &$capturedPath = null, ?string &$capturedContent = null): void { // Clear any existing File facade mock - File::clearResolvedInstances(); - File::partialMock(); + File::swap(Mockery::mock(Filesystem::class)); File::shouldReceive('ensureDirectoryExists')->once(); File::shouldReceive('exists')->andReturn($fileExists); diff --git a/tests/Unit/Support/ComposerTest.php b/tests/Unit/Support/ComposerTest.php new file mode 100644 index 00000000..04def20b --- /dev/null +++ b/tests/Unit/Support/ComposerTest.php @@ -0,0 +1,66 @@ +flush(); +}); + +it('may store and retrieve guidelines', function (): void { + $config = new Config(__DIR__); + + expect($config->getGuidelines())->toBeEmpty(); + + $guidelines = [ + 'guideline_1', + 'guideline_2', + ]; + + $config->setGuidelines($guidelines); + + expect($config->getGuidelines())->toEqual($guidelines); +}); + +it('may store and retrieve agents', function (): void { + $config = new Config(__DIR__); + + expect($config->getAgents())->toBeEmpty(); + + $agents = [ + 'agent_1', + 'agent_2', + ]; + + $config->setAgents($agents); + + expect($config->getAgents())->toEqual($agents); +}); + +it('may store and retrieve editors', function (): void { + $config = new Config(__DIR__); + + expect($config->getEditors())->toBeEmpty(); + + $editors = [ + 'editor_1', + 'editor_2', + ]; + + $config->setEditors($editors); + + expect($config->getEditors())->toEqual($editors); +}); + +it('may store and retrieve herd mcp installation status', function (): void { + $config = new Config(__DIR__); + + expect($config->getHerdMcp())->toBeFalse(); + + $config->setHerdMcp(true); + + expect($config->getHerdMcp())->toBeTrue(); + + $config->setHerdMcp(false); + + expect($config->getHerdMcp())->toBeFalse(); +}); diff --git a/tests/Unit/Support/ConfigTest.php b/tests/Unit/Support/ConfigTest.php new file mode 100644 index 00000000..4c979077 --- /dev/null +++ b/tests/Unit/Support/ConfigTest.php @@ -0,0 +1,66 @@ +flush(); +}); + +it('may store and retrieve guidelines', function (): void { + $config = new Config; + + expect($config->getGuidelines())->toBeEmpty(); + + $guidelines = [ + 'guideline_1', + 'guideline_2', + ]; + + $config->setGuidelines($guidelines); + + expect($config->getGuidelines())->toEqual($guidelines); +}); + +it('may store and retrieve agents', function (): void { + $config = new Config; + + expect($config->getAgents())->toBeEmpty(); + + $agents = [ + 'agent_1', + 'agent_2', + ]; + + $config->setAgents($agents); + + expect($config->getAgents())->toEqual($agents); +}); + +it('may store and retrieve editors', function (): void { + $config = new Config; + + expect($config->getEditors())->toBeEmpty(); + + $editors = [ + 'editor_1', + 'editor_2', + ]; + + $config->setEditors($editors); + + expect($config->getEditors())->toEqual($editors); +}); + +it('may store and retrieve herd mcp installation status', function (): void { + $config = new Config; + + expect($config->getHerdMcp())->toBeFalse(); + + $config->setHerdMcp(true); + + expect($config->getHerdMcp())->toBeTrue(); + + $config->setHerdMcp(false); + + expect($config->getHerdMcp())->toBeFalse(); +}); diff --git a/tests/Unit/UnitTest.php b/tests/Unit/UnitTest.php deleted file mode 100644 index d831a1f3..00000000 --- a/tests/Unit/UnitTest.php +++ /dev/null @@ -1,7 +0,0 @@ -toBeTrue(); -}); From d8884e0625a6220b9362f76760e965a3dd468e76 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:31:54 +0100 Subject: [PATCH 06/21] last changes --- src/BoostServiceProvider.php | 1 + src/Console/InstallCommand.php | 6 +++-- src/Console/UpdateCommand.php | 45 +++++++++++++++++++++++++++++++ src/Install/GuidelineComposer.php | 2 +- src/Support/Config.php | 4 ++- 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/Console/UpdateCommand.php diff --git a/src/BoostServiceProvider.php b/src/BoostServiceProvider.php index 4baa5c2d..4633d3ba 100644 --- a/src/BoostServiceProvider.php +++ b/src/BoostServiceProvider.php @@ -91,6 +91,7 @@ protected function registerCommands(): void $this->commands([ Console\StartCommand::class, Console\InstallCommand::class, + Console\UpdateCommand::class, Console\ExecuteToolCommand::class, ]); } diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 78dc022f..50e83352 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -194,8 +194,10 @@ protected function outro(): void $text = 'Enjoy the boost 🚀 Next steps: '; $paddingLength = (int) (floor(($this->terminal->cols() - mb_strlen($text.$label)) / 2)) - 2; - echo "\033[42m\033[2K".str_repeat(' ', max(0, $paddingLength)); // Make the entire line have a green background - echo $this->black($this->bold($text.$link)).$this->reset(PHP_EOL).$this->reset(PHP_EOL); + $this->output->write([ + "\033[42m\033[2K".str_repeat(' ', max(0, $paddingLength)), + $this->black($this->bold($text.$link)).$this->reset(PHP_EOL).$this->reset(PHP_EOL), + ]); } protected function hyperlink(string $label, string $url): string diff --git a/src/Console/UpdateCommand.php b/src/Console/UpdateCommand.php new file mode 100644 index 00000000..fa9223e2 --- /dev/null +++ b/src/Console/UpdateCommand.php @@ -0,0 +1,45 @@ +callSilently(InstallCommand::class, [ + '--no-interaction' => true, + ]); + + $this->components->info('Boost Guidelines have been updated successfully.'); + } +} diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 7bb410ad..62b383fe 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -299,7 +299,7 @@ protected function guideline(string $path, bool $thirdParty = false): array 'path' => $path, 'custom' => str_contains($path, $this->customGuidelinePath()), 'third_party' => $thirdParty, - 'tokens' => str_word_count($rendered) * 1.3, // Rough estimate of tokens based on word count + 'tokens' => round(str_word_count($rendered) * 1.3), ]; } diff --git a/src/Support/Config.php b/src/Support/Config.php index 66ce0f2c..ba0c5c61 100644 --- a/src/Support/Config.php +++ b/src/Support/Config.php @@ -80,10 +80,12 @@ protected function get(string $key, mixed $default = null): mixed protected function set(string $key, mixed $value): void { - $config = $this->all(); + $config = array_filter($this->all()); data_set($config, $key, $value); + ksort($config); + $path = base_path(self::FILE); file_put_contents($path, Str::of(json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))->append(PHP_EOL)); From 2fd73694829adb488c9775630aeb969c3f6e1fef Mon Sep 17 00:00:00 2001 From: nunomaduro <5457236+nunomaduro@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:32:21 +0000 Subject: [PATCH 07/21] Fix code styling --- src/Console/UpdateCommand.php | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/Console/UpdateCommand.php b/src/Console/UpdateCommand.php index fa9223e2..36bee0af 100644 --- a/src/Console/UpdateCommand.php +++ b/src/Console/UpdateCommand.php @@ -4,32 +4,8 @@ namespace Laravel\Boost\Console; -use Exception; use Illuminate\Console\Command; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use InvalidArgumentException; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\McpClient; -use Laravel\Boost\Install\Cli\DisplayHelper; -use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment; -use Laravel\Boost\Install\CodeEnvironmentsDetector; -use Laravel\Boost\Install\GuidelineComposer; -use Laravel\Boost\Install\GuidelineConfig; -use Laravel\Boost\Install\GuidelineWriter; -use Laravel\Boost\Install\Herd; -use Laravel\Boost\Support\Config; -use Laravel\Prompts\Concerns\Colors; -use Laravel\Prompts\Terminal; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Finder\Finder; -use Symfony\Component\Process\Process; - -use function Laravel\Prompts\confirm; -use function Laravel\Prompts\intro; -use function Laravel\Prompts\multiselect; -use function Laravel\Prompts\note; #[AsCommand('boost:update', 'Updates Laravel Boost Guidelines to the latest version.')] class UpdateCommand extends Command From bc3168ed6e5baf2db66c49144c38ed37e6fb41ec Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:35:30 +0100 Subject: [PATCH 08/21] style --- src/Console/UpdateCommand.php | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/Console/UpdateCommand.php b/src/Console/UpdateCommand.php index fa9223e2..36bee0af 100644 --- a/src/Console/UpdateCommand.php +++ b/src/Console/UpdateCommand.php @@ -4,32 +4,8 @@ namespace Laravel\Boost\Console; -use Exception; use Illuminate\Console\Command; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use InvalidArgumentException; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\McpClient; -use Laravel\Boost\Install\Cli\DisplayHelper; -use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment; -use Laravel\Boost\Install\CodeEnvironmentsDetector; -use Laravel\Boost\Install\GuidelineComposer; -use Laravel\Boost\Install\GuidelineConfig; -use Laravel\Boost\Install\GuidelineWriter; -use Laravel\Boost\Install\Herd; -use Laravel\Boost\Support\Config; -use Laravel\Prompts\Concerns\Colors; -use Laravel\Prompts\Terminal; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Finder\Finder; -use Symfony\Component\Process\Process; - -use function Laravel\Prompts\confirm; -use function Laravel\Prompts\intro; -use function Laravel\Prompts\multiselect; -use function Laravel\Prompts\note; #[AsCommand('boost:update', 'Updates Laravel Boost Guidelines to the latest version.')] class UpdateCommand extends Command From 0cac99233144d73504d4681b4bec5209fb39f61c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:37:58 +0100 Subject: [PATCH 09/21] reverts guidelines chcanges --- .ai/inertia-react/2/forms.blade.php | 3 +-- .ai/inertia-svelte/2/forms.blade.php | 3 +-- .ai/laravel/core.blade.php | 2 +- .ai/livewire/core.blade.php | 2 +- .ai/phpunit/core.blade.php | 2 +- .ai/pint/core.blade.php | 2 +- .ai/tailwindcss/core.blade.php | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.ai/inertia-react/2/forms.blade.php b/.ai/inertia-react/2/forms.blade.php index 941707fb..ac9920f5 100644 --- a/.ai/inertia-react/2/forms.blade.php +++ b/.ai/inertia-react/2/forms.blade.php @@ -1,8 +1,7 @@ -## Inertia + React Forms - @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp +## Inertia + React Forms @if($assist->inertia()->hasFormComponent()) @boostsnippet("`` Component Example", "react") diff --git a/.ai/inertia-svelte/2/forms.blade.php b/.ai/inertia-svelte/2/forms.blade.php index 518f7dce..2c0e2074 100644 --- a/.ai/inertia-svelte/2/forms.blade.php +++ b/.ai/inertia-svelte/2/forms.blade.php @@ -1,8 +1,7 @@ -## Inertia + Svelte Forms - @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp +## Inertia + Svelte Forms - There are critical differences between Svelte 4 and 5, use the `search-docs` tool for up-to-date guidance. diff --git a/.ai/laravel/core.blade.php b/.ai/laravel/core.blade.php index 4fd16344..6c27ba6b 100644 --- a/.ai/laravel/core.blade.php +++ b/.ai/laravel/core.blade.php @@ -1,4 +1,4 @@ -## Laravel +## Do Things the Laravel Way - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. - If you're creating a generic PHP class, use `artisan make:class`. diff --git a/.ai/livewire/core.blade.php b/.ai/livewire/core.blade.php index 1152d327..9d7e532e 100644 --- a/.ai/livewire/core.blade.php +++ b/.ai/livewire/core.blade.php @@ -1,4 +1,4 @@ -## Livewire +## Livewire Core - Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. - Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components - State should live on the server, with the UI reflecting it. diff --git a/.ai/phpunit/core.blade.php b/.ai/phpunit/core.blade.php index 703a678e..946ae9f2 100644 --- a/.ai/phpunit/core.blade.php +++ b/.ai/phpunit/core.blade.php @@ -1,4 +1,4 @@ -## PHPUnit +## PHPUnit Core - This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit ` to create a new test. - If you see a test using "Pest", convert it to PHPUnit. diff --git a/.ai/pint/core.blade.php b/.ai/pint/core.blade.php index 71ff48c1..0283e0ac 100644 --- a/.ai/pint/core.blade.php +++ b/.ai/pint/core.blade.php @@ -1,4 +1,4 @@ -## Laravel Pint +## Laravel Pint Code Formatter - You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. - Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. diff --git a/.ai/tailwindcss/core.blade.php b/.ai/tailwindcss/core.blade.php index 46bcc237..522c4a60 100644 --- a/.ai/tailwindcss/core.blade.php +++ b/.ai/tailwindcss/core.blade.php @@ -1,4 +1,4 @@ -## Tailwind +## Tailwind Core - Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. - Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) From 416e9bcbdd7852c478905db584bd64ec72aab55b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:38:35 +0100 Subject: [PATCH 10/21] stylee --- .ai/foundation.blade.php | 2 +- .ai/inertia-vue/2/forms.blade.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.ai/foundation.blade.php b/.ai/foundation.blade.php index a6f7a71d..8dbc49ba 100644 --- a/.ai/foundation.blade.php +++ b/.ai/foundation.blade.php @@ -1,4 +1,4 @@ -# Foundation (Recommended) +# Laravel Boost Guidelines The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. diff --git a/.ai/inertia-vue/2/forms.blade.php b/.ai/inertia-vue/2/forms.blade.php index 56bc041a..9aa29627 100644 --- a/.ai/inertia-vue/2/forms.blade.php +++ b/.ai/inertia-vue/2/forms.blade.php @@ -1,8 +1,7 @@ -## Inertia + Vue Forms - @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp +## Inertia + Vue Forms @if($assist->inertia()->hasFormComponent()) @boostsnippet("`` Component Example", "vue") From d9d5770874510dc3c62ed0ffc4f080759eb15e1b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:41:32 +0100 Subject: [PATCH 11/21] wip --- tests/Pest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Pest.php b/tests/Pest.php index 834f4103..7b6c4245 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -16,10 +16,7 @@ use Laravel\Boost\Support\Config; use Laravel\Mcp\Response; -uses(Tests\TestCase::class)->in('Unit', 'Feature') - ->beforeEach(function () { - (new Config)->flush(); - }); +uses(Tests\TestCase::class)->in('Unit', 'Feature'); expect()->extend('isToolResult', fn () => $this->toBeInstanceOf(Response::class)); From 005d89cae2b0b7b6730e38efb8c747c107418c82 Mon Sep 17 00:00:00 2001 From: nunomaduro <5457236+nunomaduro@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:41:54 +0000 Subject: [PATCH 12/21] Fix code styling --- tests/Pest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Pest.php b/tests/Pest.php index 7b6c4245..79ba034a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,7 +13,6 @@ | */ -use Laravel\Boost\Support\Config; use Laravel\Mcp\Response; uses(Tests\TestCase::class)->in('Unit', 'Feature'); From c71a102c390514578a78f3e53a683843f1439b85 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 Sep 2025 17:42:46 +0100 Subject: [PATCH 13/21] remove final --- src/Install/CodeEnvironment/CodeEnvironment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 0dcdba30..4082a47e 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -93,7 +93,7 @@ public function mcpInstallationStrategy(): McpInstallationStrategy return McpInstallationStrategy::FILE; } - final public static function fromName(string $name): ?static + public static function fromName(string $name): ?static { $detectionFactory = app(DetectionStrategyFactory::class); From 6a7fb847ca6df48abe95a79850e560c5921ba980 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 26 Sep 2025 13:40:41 -0500 Subject: [PATCH 14/21] Update UpdateCommand.php --- src/Console/UpdateCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Console/UpdateCommand.php b/src/Console/UpdateCommand.php index 36bee0af..118754b4 100644 --- a/src/Console/UpdateCommand.php +++ b/src/Console/UpdateCommand.php @@ -7,7 +7,7 @@ use Illuminate\Console\Command; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand('boost:update', 'Updates Laravel Boost Guidelines to the latest version.')] +#[AsCommand('boost:update', 'Updates Laravel Boost guidelines to the latest version')] class UpdateCommand extends Command { public function handle(): void @@ -16,6 +16,6 @@ public function handle(): void '--no-interaction' => true, ]); - $this->components->info('Boost Guidelines have been updated successfully.'); + $this->components->info('Boost guidelines updated successfully.'); } } From 627a45156c37f3b463b7ad04d4de012c5b0e4d1b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 29 Sep 2025 10:02:18 +0100 Subject: [PATCH 15/21] fix: no description --- src/Install/GuidelineComposer.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 62b383fe..603ad258 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -287,9 +287,12 @@ protected function guideline(string $path, bool $thirdParty = false): array $this->storedSnippets = []; // Clear for next use - $description = Str::of($rendered)->after('# ')->before("\n")->trim() - ->whenEmpty(fn () => 'No description provided') + $description = Str::of($rendered) + ->after('# ') + ->before("\n") + ->trim() ->limit(50, '...') + ->whenEmpty(fn () => 'No description provided') ->value(); return [ From 517fcd1825ea3c930727748098425856f5872020 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 29 Sep 2025 10:50:05 +0100 Subject: [PATCH 16/21] fix --- src/Install/GuidelineComposer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 603ad258..ff59c572 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -292,7 +292,7 @@ protected function guideline(string $path, bool $thirdParty = false): array ->before("\n") ->trim() ->limit(50, '...') - ->whenEmpty(fn () => 'No description provided') + ->whenEmpty(fn () => Str::of('No description provided')) ->value(); return [ From 8ef12fb6d7830a0fcc5be6dbefe7eee4fe48821c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 29 Sep 2025 11:08:20 +0100 Subject: [PATCH 17/21] adds docs --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 9c4bfcdf..3174bdce 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,25 @@ Laravel Boost includes AI guidelines for the following packages and frameworks. | Laravel Folio | core | | Enforce Tests | conditional | +### Keeping Guidelines Up-to-Date + +You may want to periodically update your local AI guidelines to ensure they reflect the latest versions of the Laravel ecosystem packages you have installed. To do so, you can use the `boost:update` Artisan command. + +```bash +php artisan boost:update +``` + +You may also automate this process by adding it to your Composer "post-update-cmd" scripts: + +```json +{ + "scripts": { + "post-update-cmd": [ + "@php artisan boost:update --ansi" + ] + } +} +``` ## Available Documentation @@ -106,6 +125,29 @@ You can override Boost's built-in AI guidelines by creating your own custom guid For example, to override Boost's "Inertia React v2 Form Guidance" guidelines, create a file at `.ai/guidelines/inertia-react/2/forms.blade.php`. When you run `boost:install`, Boost will include your custom guideline instead of the default one. +## Third-Party Package AI Guidelines + +If you maintain a third-party package and would like Boost to include AI guidelines for it, you can do so by adding a `resources/boost/guidelines/core.blade.php` file to your package. When users of your package run `php artisan boost:install`, Boost will automatically load your guidelines. + +AI guidelines should provide a short overview of what your package does, outline any required file structure or conventions, and explain how to create or use its main features (with example commands or code snippets). Keep them concise, actionable, and focused on best practices so AI can generate correct code for your users. Here is an example: + +```php +## Package Name + +This package provides [brief description of functionality]. + +### Features + +- Feature 1: [clear & short description]. +- Feature 2: [clear & short description]. Example usage: + +@verbatim + +$result = PackageName::featureTwo($param1, $param2); + +@endverbatim +``` + ## Manually Registering the Boost MCP Server Sometimes you may need to manually register the Laravel Boost MCP server with your editor of choice. You should register the MCP server using the following details: From 79fe9e37eeb44b2a3d84de8570ff8cedc0a52467 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 29 Sep 2025 11:12:38 +0100 Subject: [PATCH 18/21] Update UpdateCommand.php --- src/Console/UpdateCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/UpdateCommand.php b/src/Console/UpdateCommand.php index 118754b4..6e6fbf46 100644 --- a/src/Console/UpdateCommand.php +++ b/src/Console/UpdateCommand.php @@ -7,7 +7,7 @@ use Illuminate\Console\Command; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand('boost:update', 'Updates Laravel Boost guidelines to the latest version')] +#[AsCommand('boost:update', 'Update the Laravel Boost guidelines to the latest guidance')] class UpdateCommand extends Command { public function handle(): void From 614d10fa712d6e55b4a16358367b6e9615163e8a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 29 Sep 2025 11:13:56 +0100 Subject: [PATCH 19/21] Update InstallCommand.php --- src/Console/InstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 50e83352..ca428dac 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -247,7 +247,7 @@ protected function selectBoostFeatures(): Collection if (confirm( label: 'Would you like to install Herd MCP alongside Boost MCP?', default: $this->config->getHerdMcp(), - hint: 'Herd MCP provides additional tools like browser logs, that can help AI understand issues better', + hint: 'The Herd MCP provides additional tools like browser logs, which can help AI understand issues better', )) { $features->push('herd_mcp'); } From 75d9299991f67c4ea72c0b7b8229c0aa3217c915 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 29 Sep 2025 11:14:46 +0100 Subject: [PATCH 20/21] Update InstallCommand.php --- src/Console/InstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index ca428dac..481cadd3 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -268,7 +268,7 @@ protected function selectAiGuidelines(): Collection } return collect(multiselect( - label: 'Which Third Party AI Guidelines do you want to install?', + label: 'Which third-party AI Guidelines do you want to install?', // @phpstan-ignore-next-line options: $options->mapWithKeys(function (array $guideline, string $name) { $humanName = str_replace('/core', '', $name); From f09782ca0278c7a0a6aa437a823756a91f348a01 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 29 Sep 2025 11:14:59 +0100 Subject: [PATCH 21/21] Update InstallCommand.php --- src/Console/InstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 481cadd3..212ec264 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -268,7 +268,7 @@ protected function selectAiGuidelines(): Collection } return collect(multiselect( - label: 'Which third-party AI Guidelines do you want to install?', + label: 'Which third-party AI guidelines do you want to install?', // @phpstan-ignore-next-line options: $options->mapWithKeys(function (array $guideline, string $name) { $humanName = str_replace('/core', '', $name);