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'),