diff --git a/.ai/enforce-tests.blade.php b/.ai/enforce-tests.blade.php index 3b05821e..91b0f9c4 100644 --- a/.ai/enforce-tests.blade.php +++ b/.ai/enforce-tests.blade.php @@ -1,4 +1,7 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## Test Enforcement - Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. +- Run the minimum number of tests needed to ensure code quality and speed. Use `{{ $assist->artisan() }} test` with a specific filename or filter. diff --git a/.ai/filament/3/core.blade.php b/.ai/filament/3/core.blade.php index 68f4d2b0..9c08aded 100644 --- a/.ai/filament/3/core.blade.php +++ b/.ai/filament/3/core.blade.php @@ -1,3 +1,6 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## Filament 3 ## Version 3 Changes To Focus On @@ -7,5 +10,5 @@ - Tables use the `Tables\Columns` namespace for table columns. - A new `Filament\Forms\Components\RichEditor` component is available. - Form and table schemas now use fluent method chaining. -- Added `php artisan filament:optimize` command for production optimization. +- Added `{{ $assist->artisan() }} filament:optimize` command for production optimization. - Requires implementing `FilamentUser` contract for production access control. diff --git a/.ai/filament/core.blade.php b/.ai/filament/core.blade.php index a5a36ee9..5591feb7 100644 --- a/.ai/filament/core.blade.php +++ b/.ai/filament/core.blade.php @@ -1,3 +1,6 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## Filament - Filament is used by this application, check how and where to follow existing application conventions. - Filament is a Server-Driven UI (SDUI) framework for Laravel. It allows developers to define user interfaces in PHP using structured configuration objects. It is built on top of Livewire, Alpine.js, and Tailwind CSS. @@ -5,7 +8,7 @@ - Utilize static `make()` methods for consistent component initialization. ### Artisan -- You must use the Filament specific Artisan commands to create new files or components for Filament. You can find these with the `list-artisan-commands` tool, or with `php artisan` and the `--help` option. +- You must use the Filament specific Artisan commands to create new files or components for Filament. You can find these with the `list-artisan-commands` tool, or with `{{ $assist->artisan() }}` and the `--help` option. - Inspect the required options, always pass `--no-interaction`, and valid arguments for other options when applicable. ### Filament's Core Features diff --git a/.ai/folio/core.blade.php b/.ai/folio/core.blade.php index b4ac9c17..44f25369 100644 --- a/.ai/folio/core.blade.php +++ b/.ai/folio/core.blade.php @@ -1,23 +1,24 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## Laravel Folio - Laravel Folio is a file based router. With Laravel Folio, a new route is created for every Blade file within the configured Folio directory. For example, pages are usually in in `resources/views/pages/` and the file structure determines routes: - `pages/index.blade.php` → `/` - `pages/profile/index.blade.php` → `/profile` - `pages/auth/login.blade.php` → `/auth/login` -- You may list available Folio routes using `php artisan folio:list` or using Boost's `list-routes` tool. +- You may list available Folio routes using `{{ $assist->artisan() }} folio:list` or using Boost's `list-routes` tool. ### New Pages & Routes -- Always create new `folio` pages and routes using `artisan folio:page [name]` following existing naming conventions. +- Always create new `folio` pages and routes using `{{ $assist->artisan() }} folio:page [name]` following existing naming conventions. -@verbatim // Creates: resources/views/pages/products.blade.php → /products - php artisan folio:page 'products' + {{ $assist->artisan() }} folio:page 'products' // Creates: resources/views/pages/products/[id].blade.php → /products/{id} - php artisan folio:page 'products/[id]' + {{ $assist->artisan() }} folio:page 'products/[id]' -@endverbatim - Add a 'name' to each new Folio page at the very top of the file so it has a named route available for other parts of the codebase to use. diff --git a/.ai/foundation.blade.php b/.ai/foundation.blade.php index c715fd8a..e6f68439 100644 --- a/.ai/foundation.blade.php +++ b/.ai/foundation.blade.php @@ -1,3 +1,6 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp # 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. @@ -27,7 +30,11 @@ - Do not change the application's dependencies without approval. ## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `{{ $assist->nodePackageManager() }} run build`, `{{ $assist->nodePackageManager() }} run dev`, or `composer run dev`. Ask them. +@if ($assist->config->enforceSail) +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `{{ $assist->composer() }} run dev`. Ask them. +@else +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `{{ $assist->nodePackageManager() }} run build`, `{{ $assist->nodePackageManager() }} run dev`, or `{{ $assist->composer() }} run dev`. Ask them. +@endif ## Replies - Be concise in your explanations - focus on what's important rather than explaining obvious details. diff --git a/.ai/laravel/11/core.blade.php b/.ai/laravel/11/core.blade.php index 97f547a8..ee28b469 100644 --- a/.ai/laravel/11/core.blade.php +++ b/.ai/laravel/11/core.blade.php @@ -32,6 +32,6 @@ ### New Artisan Commands - List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11: - - `php artisan make:enum` - - `php artisan make:class` - - `php artisan make:interface` + - `{{ $assist->artisan() }} make:enum` + - `{{ $assist->artisan() }} make:class` + - `{{ $assist->artisan() }} make:interface` diff --git a/.ai/laravel/core.blade.php b/.ai/laravel/core.blade.php index de915496..ff4818ca 100644 --- a/.ai/laravel/core.blade.php +++ b/.ai/laravel/core.blade.php @@ -1,7 +1,10 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## 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`. +- Use `{{ $assist->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 `{{ $assist->artisan() }} make:class`. - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. ### Database @@ -12,7 +15,7 @@ - Use Laravel's query builder for very complex database operations. ### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `{{ $assist->artisan() }} make:model`. ### APIs & Eloquent Resources - For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. @@ -36,7 +39,11 @@ ### Testing - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. +- When creating tests, make use of `{{ $assist->artisan() }} make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. ### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `{{ $assist->nodePackageManager() }} run build` or ask the user to run `{{ $assist->nodePackageManager() }} run dev` or `composer run dev`. +@if ($assist->config->enforceSail) +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `{{ $assist->composer() }} run dev` or ask the user to run it. +@else +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `{{ $assist->nodePackageManager() }} run build` or ask the user to run `{{ $assist->nodePackageManager() }} run dev` or `{{ $assist->composer() }} run dev`. +@endif diff --git a/.ai/livewire/core.blade.php b/.ai/livewire/core.blade.php index 9d7e532e..07f4c067 100644 --- a/.ai/livewire/core.blade.php +++ b/.ai/livewire/core.blade.php @@ -1,6 +1,9 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## 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 +- Use the `{{ $assist->artisan() }} make:livewire [Posts\\CreatePost]` artisan command to create new components - State should live on the server, with the UI reflecting it. - All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. diff --git a/.ai/pest/core.blade.php b/.ai/pest/core.blade.php index 818b16d6..4586d325 100644 --- a/.ai/pest/core.blade.php +++ b/.ai/pest/core.blade.php @@ -1,10 +1,12 @@ ## Pest - +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ### Testing - If you need to verify a feature is working, write or update a Unit / Feature test. ### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. +- All tests must be written using Pest. Use `{{ $assist->artisan() }} make:test --pest `. - You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. - Tests should test all of the happy paths, failure paths, and weird paths. - Tests live in the `tests/Feature` and `tests/Unit` directories. @@ -17,9 +19,9 @@ ### Running Tests - Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). +- To run all tests: `{{ $assist->artisan() }} test`. +- To run all tests in a file: `{{ $assist->artisan() }} test tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `{{ $assist->artisan() }} test --filter=testName` (recommended after making a change to a related file). - When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. ### Pest Assertions diff --git a/.ai/phpunit/core.blade.php b/.ai/phpunit/core.blade.php index 946ae9f2..d4bab859 100644 --- a/.ai/phpunit/core.blade.php +++ b/.ai/phpunit/core.blade.php @@ -1,6 +1,9 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## 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. +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `{{ $assist->artisan() }} make:test --phpunit ` to create a new test. - If you see a test using "Pest", convert it to PHPUnit. - Every time a test has been updated, run that singular test. - When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. @@ -9,6 +12,6 @@ ### Running Tests - Run the minimal number of tests, using an appropriate filter, before finalizing. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). +- To run all tests: `{{ $assist->artisan() }} test`. +- To run all tests in a file: `{{ $assist->artisan() }} test tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `{{ $assist->artisan() }} test --filter=testName` (recommended after making a change to a related file). diff --git a/.ai/pint/core.blade.php b/.ai/pint/core.blade.php index 0283e0ac..1e81775b 100644 --- a/.ai/pint/core.blade.php +++ b/.ai/pint/core.blade.php @@ -1,4 +1,7 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## 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. +- You must run `{{ $assist->bin() }}pint --dirty` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `{{ $assist->bin() }}pint --test`, simply run `{{ $assist->bin() }}pint` to fix any formatting issues. diff --git a/.ai/sail/core.blade.php b/.ai/sail/core.blade.php new file mode 100644 index 00000000..43551993 --- /dev/null +++ b/.ai/sail/core.blade.php @@ -0,0 +1,11 @@ +## Laravel Sail + +- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail. +- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`. +- Open the application in the browser by running `vendor/bin/sail open`. +- Always prefix PHP, Artisan, Composer, and Node commands** with `vendor/bin/sail`. Examples: + - Run migrations: `vendor/bin/sail artisan migrate` + - Install Composer packages: `vendor/bin/sail composer install` + - Run npm: `vendor/bin/sail npm run dev` + - Execute PHP scripts: `vendor/bin/sail php [script]` +- View all available Sail commands by running `vendor/bin/sail` without arguments. diff --git a/.ai/volt/core.blade.php b/.ai/volt/core.blade.php index 483ee765..62b4a0d5 100644 --- a/.ai/volt/core.blade.php +++ b/.ai/volt/core.blade.php @@ -1,7 +1,10 @@ +@php +/** @var \Laravel\Boost\Install\GuidelineAssist $assist */ +@endphp ## Livewire Volt - This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it. -- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]` +- Make new Volt components using `{{ $assist->artisan() }} make:volt [name] [--test] [--pest]` - Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file - Livewire Volt allows PHP logic and Blade templates in one file. Components use the @verbatim`@volt`@endverbatim directive. - You must check existing Volt components to determine if they're functional or class based. If you can't detect that, ask the user which they prefer before writing a Volt component. diff --git a/all.php b/all.php deleted file mode 100644 index 0469f1a9..00000000 --- a/all.php +++ /dev/null @@ -1,126 +0,0 @@ - [ - 'APP_URL=http://localhost.test', - ], -]), options: ['enables_package_discoveries' => false]); - -// Create a mock Roster that returns ALL packages from .ai/ directory -$mockRoster = new class extends Roster -{ - public function packages(): \Laravel\Roster\PackageCollection - { - $packages = []; - - // Find all package directories in .ai/ - $directories = glob(__DIR__.'/.ai/*', GLOB_ONLYDIR); - - foreach ($directories as $dir) { - $packageName = basename($dir); - - // Skip special directories handled elsewhere in GuidelineComposer - if (in_array($packageName, ['boost', 'herd'], true)) { - continue; - } - - // Map directory names to Roster enum values where they exist - $enumMapping = [ - 'php' => \Laravel\Roster\Enums\Packages::LARAVEL, // Use Laravel as placeholder for php - 'laravel' => \Laravel\Roster\Enums\Packages::LARAVEL, - 'filament' => \Laravel\Roster\Enums\Packages::FILAMENT, - 'fluxui-free' => \Laravel\Roster\Enums\Packages::FLUXUI_FREE, - 'fluxui-pro' => \Laravel\Roster\Enums\Packages::FLUXUI_PRO, - 'inertia-laravel' => \Laravel\Roster\Enums\Packages::INERTIA_LARAVEL, - 'inertia-react' => \Laravel\Roster\Enums\Packages::INERTIA_REACT, - 'inertia-vue' => \Laravel\Roster\Enums\Packages::INERTIA_VUE, - 'livewire' => \Laravel\Roster\Enums\Packages::LIVEWIRE, - 'pest' => \Laravel\Roster\Enums\Packages::PEST, - 'phpunit' => \Laravel\Roster\Enums\Packages::PHPUNIT, - 'pint' => \Laravel\Roster\Enums\Packages::PINT, - 'volt' => \Laravel\Roster\Enums\Packages::VOLT, - 'folio' => \Laravel\Roster\Enums\Packages::FOLIO, - 'pennant' => \Laravel\Roster\Enums\Packages::PENNANT, - 'tailwindcss' => \Laravel\Roster\Enums\Packages::TAILWINDCSS, - 'wayfinder' => \Laravel\Roster\Enums\Packages::WAYFINDER, - ]; - - if (isset($enumMapping[$packageName])) { - // Find ALL version directories and create a package for each - $versionDirs = glob(__DIR__."/.ai/{$packageName}/*", GLOB_ONLYDIR); - if (! empty($versionDirs)) { - $versions = array_map('basename', $versionDirs); - sort($versions, SORT_NUMERIC); - - // Create a package instance for each version found - foreach ($versions as $versionNumber) { - $packages[] = new \Laravel\Roster\Package( - $enumMapping[$packageName], - $packageName, - $versionNumber.'.0.0', - false - ); - } - } else { - // No version directories, just add the core package - $packages[] = new \Laravel\Roster\Package( - $enumMapping[$packageName], - $packageName, - '1.0.0', - false - ); - } - } - } - - return new \Laravel\Roster\PackageCollection($packages); - } -}; - -$herd = new Herd; - -// Create GuidelineComposer with all config options enabled to get ALL guidelines -$config = new GuidelineConfig; -$config->laravelStyle = true; -$config->hasAnApi = true; -$config->caresAboutLocalization = true; -$config->enforceTests = true; - -// Use the real GuidelineComposer with our mock Roster - this will use the exact same ordering logic -$composer = new GuidelineComposer($mockRoster, $herd); -$composer->config($config); - -// Get the guidelines that GuidelineComposer would normally find -$guidelines = $composer->guidelines(); - -// Add missing PHP versions (since GuidelineComposer only adds current PHP version) -$reflection = new ReflectionClass($composer); -$guidelineDirMethod = $reflection->getMethod('guidelinesDir'); -$guidelineDirMethod->setAccessible(true); - -$phpVersions = ['8.1', '8.2', '8.3', '8.4']; -$currentPhp = PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION; - -foreach ($phpVersions as $phpVersion) { - if ($phpVersion !== $currentPhp) { - $content = $guidelineDirMethod->invoke($composer, "php/{$phpVersion}"); - if (! empty($content)) { - $guidelines->put("php/v{$phpVersion}", $content); - } - } -} - -// Now compose ALL guidelines (original + missing PHP versions) -echo GuidelineComposer::composeGuidelines($guidelines); diff --git a/rector.php b/rector.php index 8df4c89b..b9c1690f 100644 --- a/rector.php +++ b/rector.php @@ -17,9 +17,7 @@ ReadOnlyPropertyRector::class, EncapsedStringsToSprintfRector::class, DisallowedEmptyRuleFixerRector::class, - FunctionLikeToFirstClassCallableRector::class => [ - __DIR__.'src/Install/CodeEnvironmentsDetector.php', - ], + FunctionLikeToFirstClassCallableRector::class, ]) ->withPreparedSets( deadCode: true, diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 515fe0ac..02d95656 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\Install\Sail; use Laravel\Boost\Support\Config; use Laravel\Prompts\Concerns\Colors; use Laravel\Prompts\Terminal; @@ -40,6 +41,8 @@ class InstallCommand extends Command private Herd $herd; + private Sail $sail; + private Terminal $terminal; /** @var Collection */ @@ -81,6 +84,7 @@ public function __construct(protected Config $config) public function handle( CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, + Sail $sail, Terminal $terminal, ): int { $this->installGuidelines = ! $this->option('ignore-guidelines'); @@ -92,7 +96,7 @@ public function handle( return self::FAILURE; } - $this->bootstrap($codeEnvironmentsDetector, $herd, $terminal); + $this->bootstrap($codeEnvironmentsDetector, $herd, $sail, $terminal); $this->displayBoostHeader(); $this->discoverEnvironment(); $this->collectInstallationPreferences(); @@ -102,10 +106,11 @@ public function handle( return self::SUCCESS; } - protected function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, Terminal $terminal): void + protected function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $herd, Sail $sail, Terminal $terminal): void { $this->codeEnvironmentsDetector = $codeEnvironmentsDetector; $this->herd = $herd; + $this->sail = $sail; $this->terminal = $terminal; $this->terminal->initDimensions(); @@ -268,7 +273,7 @@ protected function selectBoostFeatures(): Collection $features->push('herd_mcp'); } - if ($this->isSailInstalled() && ($this->isRunningInsideSail() || $this->shouldConfigureSail())) { + if ($this->sail->isInstalled() && ($this->sail->isActive() || $this->shouldConfigureSail())) { $features->push('sail'); } @@ -442,6 +447,7 @@ protected function installGuidelines(): void $guidelineConfig->caresAboutLocalization = $this->detectLocalization(); $guidelineConfig->hasAnApi = false; $guidelineConfig->aiGuidelines = $this->selectedAiGuidelines->values()->toArray(); + $guidelineConfig->enforceSail = $this->shouldUseSail(); $composer = app(GuidelineComposer::class)->config($guidelineConfig); $guidelines = $composer->guidelines(); @@ -530,21 +536,10 @@ protected function shouldUseSail(): bool return $this->selectedBoostFeatures->contains('sail'); } - protected function isSailInstalled(): bool - { - return file_exists(base_path('vendor/bin/sail')) && - (file_exists(base_path('docker-compose.yml')) || file_exists(base_path('compose.yaml'))); - } - - protected function isRunningInsideSail(): bool - { - return get_current_user() === 'sail' || getenv('LARAVEL_SAIL') === '1'; - } - protected function buildMcpCommand(McpClient $mcpClient): array { if ($this->shouldUseSail()) { - return ['laravel-boost', './vendor/bin/sail', 'artisan', 'boost:mcp']; + return ['laravel-boost', Sail::SAIL_BINARY_PATH, 'artisan', 'boost:mcp']; } $inWsl = $this->isRunningInWsl(); diff --git a/src/Install/GuidelineAssist.php b/src/Install/GuidelineAssist.php index 6b06f12f..afe925d9 100644 --- a/src/Install/GuidelineAssist.php +++ b/src/Install/GuidelineAssist.php @@ -7,7 +7,6 @@ use Illuminate\Database\Eloquent\Model; use Laravel\Boost\Install\Assists\Inertia; use Laravel\Roster\Enums\NodePackageManager; -use Laravel\Roster\Enums\Packages; use Laravel\Roster\Roster; use ReflectionClass; use Symfony\Component\Finder\Finder; @@ -24,7 +23,7 @@ class GuidelineAssist protected static array $classes = []; - public function __construct(public Roster $roster) + public function __construct(public Roster $roster, public GuidelineConfig $config) { $this->modelPaths = $this->discover(fn ($reflection): bool => ($reflection->isSubclassOf(Model::class) && ! $reflection->isAbstract())); $this->controllerPaths = $this->discover(fn (ReflectionClass $reflection): bool => (stripos($reflection->getName(), 'controller') !== false || stripos($reflection->getNamespaceName(), 'controller') !== false)); @@ -160,11 +159,6 @@ public function enumContents(): string return file_get_contents(current($this->enumPaths)); } - public function packageGte(Packages $package, string $version): bool - { - return $this->roster->usesVersion($package, $version, '>='); - } - public function inertia(): Inertia { return new Inertia($this->roster); @@ -174,4 +168,25 @@ public function nodePackageManager(): string { return ($this->roster->nodePackageManager() ?? NodePackageManager::NPM)->value; } + + public function artisan(): string + { + return $this->config->enforceSail + ? Sail::SAIL_BINARY_PATH.' artisan' + : 'php artisan'; + } + + public function composer(): string + { + return $this->config->enforceSail + ? Sail::SAIL_BINARY_PATH.' composer' + : 'composer'; + } + + public function bin(): string + { + return $this->config->enforceSail + ? Sail::SAIL_BINARY_PATH.' bin ' + : 'vendor/bin/'; + } } diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 2a957af1..bd7ab9b1 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -24,8 +24,6 @@ class GuidelineComposer protected GuidelineConfig $config; - protected GuidelineAssist $guidelineAssist; - /** * Package priority system to handle conflicts between packages. * When a higher-priority package is present, lower-priority packages are excluded from guidelines. @@ -44,6 +42,16 @@ class GuidelineComposer Packages::MCP, ]; + /** + * Packages that should be excluded from automatic guideline inclusion. + * These packages require explicit configuration to be included. + * + * @var array + */ + protected array $optInPackages = [ + Packages::SAIL, + ]; + public function __construct(protected Roster $roster, protected Herd $herd) { $this->packagePriorities = [ @@ -51,7 +59,6 @@ public function __construct(protected Roster $roster, protected Herd $herd) Packages::FLUXUI_PRO->value => [Packages::FLUXUI_FREE->value], ]; $this->config = new GuidelineConfig; - $this->guidelineAssist = new GuidelineAssist($roster); } public function config(GuidelineConfig $config): self @@ -125,10 +132,14 @@ protected function find(): Collection // $phpMajorMinor = PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION; // $guidelines->put('php/v'.$phpMajorMinor, $this->guidelinesDir('php/'.$phpMajorMinor)); - if (str_contains((string) config('app.url'), '.test') && $this->herd->isInstalled()) { + if (str_contains((string) config('app.url'), '.test') && $this->herd->isInstalled() && ! $this->config->enforceSail) { $guidelines->put('herd', $this->guideline('herd/core')); } + if ($this->config->enforceSail) { + $guidelines->put('sail', $this->guideline('sail/core')); + } + if ($this->config->laravelStyle) { $guidelines->put('laravel/style', $this->guideline('laravel/style')); } @@ -209,10 +220,17 @@ protected function find(): Collection */ protected function shouldExcludePackage(Package $package): bool { + if (in_array($package->package(), $this->optInPackages, true)) { + return true; + } + foreach ($this->packagePriorities as $priorityPackage => $excludedPackages) { - if (in_array($package->package()->value, $excludedPackages, true)) { - $priorityEnum = Packages::from($priorityPackage); - if ($this->roster->uses($priorityEnum)) { + $packageIsInExclusionList = in_array($package->package()->value, $excludedPackages, true); + + if ($packageIsInExclusionList) { + $priorityPackageExists = $this->roster->uses(Packages::from($priorityPackage)); + + if ($priorityPackageExists) { return true; } } @@ -262,7 +280,7 @@ protected function renderContent(string $content, string $path): string $content = str_replace(array_keys($placeholders), array_values($placeholders), $content); $rendered = Blade::render($content, [ - 'assist' => $this->guidelineAssist, + 'assist' => $this->getGuidelineAssist(), ]); return str_replace(array_values($placeholders), array_keys($placeholders), $rendered); @@ -330,6 +348,11 @@ protected function processBoostSnippets(string $content): string }, $content); } + protected function getGuidelineAssist(): GuidelineAssist + { + return new GuidelineAssist($this->roster, $this->config); + } + protected function prependPackageGuidelinePath(string $path): string { return $this->prependGuidelinePath($path, __DIR__.'/../../.ai/'); diff --git a/src/Install/GuidelineConfig.php b/src/Install/GuidelineConfig.php index 20baccbf..fa0b1cb7 100644 --- a/src/Install/GuidelineConfig.php +++ b/src/Install/GuidelineConfig.php @@ -10,6 +10,8 @@ class GuidelineConfig public bool $laravelStyle = false; + public bool $enforceSail = false; + public bool $caresAboutLocalization = false; public bool $hasAnApi = false; diff --git a/src/Install/Sail.php b/src/Install/Sail.php new file mode 100644 index 00000000..a2e7b5e8 --- /dev/null +++ b/src/Install/Sail.php @@ -0,0 +1,23 @@ + ['http://localhost:8000', true, false], ]); +test('excludes Herd guidelines when Sail is configured', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + $this->herd->shouldReceive('isInstalled')->andReturn(true); + + config(['app.url' => 'http://myapp.test']); + + $config = new GuidelineConfig; + $config->enforceSail = true; + + $guidelines = $this->composer + ->config($config) + ->compose(); + + expect($guidelines) + ->not->toContain('Laravel Herd') + ->toContain('Laravel Sail'); + +}); + +test('excludes Sail guidelines when Herd is configured', function (): void { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + $this->herd->shouldReceive('isInstalled')->andReturn(true); + + config(['app.url' => 'http://myapp.test']); + + $config = new GuidelineConfig; + $config->enforceSail = false; + + $guidelines = $this->composer + ->config($config) + ->compose(); + + expect($guidelines) + ->toContain('Laravel Herd') + ->not->toContain('Laravel Sail'); +}); + test('composes guidelines with proper formatting', function (): void { $packages = new PackageCollection([ new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'),