From 75c0dd25932fbe084ef85161f1da637613e05f7e Mon Sep 17 00:00:00 2001 From: Sagar Naliyapara Date: Fri, 8 May 2026 15:53:36 +0530 Subject: [PATCH 1/2] Add optional .gitignore AI artifact rules to boost install --- src/Console/InstallCommand.php | 98 ++++++++++++++++ .../Console/InstallCommandGitignoreTest.php | 106 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 tests/Feature/Console/InstallCommandGitignoreTest.php diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index ceb7058b..eeee41fb 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -88,6 +88,7 @@ public function handle(): int $this->discoverEnvironment(); $this->collectInstallationPreferences(); $this->performInstallation(); + $this->updateGitignore(); $this->outro(); return self::SUCCESS; @@ -140,6 +141,103 @@ protected function performInstallation(): void $this->storeConfig(); } + protected function updateGitignore(): void + { + if (! $this->input->isInteractive()) { + $this->info('Skipped .gitignore updates.'); + + return; + } + + if (! $this->confirm('Would you like to add recommended AI artifacts to .gitignore?')) { + $this->info('Skipped .gitignore updates.'); + + return; + } + + $path = base_path('.gitignore'); + $content = file_exists($path) ? file_get_contents($path) : ''; + $updatedContent = $this->mergeBoostGitignoreRules($content ?: ''); + + if ($updatedContent === $content) { + $this->info('Laravel Boost ignore rules are already present in .gitignore.'); + + return; + } + + file_put_contents($path, $updatedContent); + + $this->info('Added Laravel Boost ignore rules to .gitignore.'); + } + + protected function mergeBoostGitignoreRules(string $content): string + { + $lineEnding = $this->detectLineEnding($content); + $lines = $this->gitignoreLines($content); + $normalizedLines = collect($lines)->map(fn (string $line): string => trim($line)); + $entries = collect([ + '.ai/generated', + '.ai/cache', + '.claude/', + '.cursor/rules/generated', + ]); + + $missingEntries = $entries + ->reject(fn (string $entry): bool => $normalizedLines->contains($entry)) + ->values(); + + if ($missingEntries->isEmpty()) { + return $content; + } + + if ($normalizedLines->contains('# Laravel Boost')) { + $sectionStart = $normalizedLines->search('# Laravel Boost'); + $sectionEnd = $sectionStart + 1; + + while (isset($lines[$sectionEnd]) && $entries->contains(trim($lines[$sectionEnd]))) { + $sectionEnd++; + } + + array_splice($lines, $sectionEnd, 0, $missingEntries->all()); + } else { + $lines = [ + ...$lines, + ...($lines === [] ? [] : ['']), + '# Laravel Boost', + ...$missingEntries->all(), + ]; + } + + return implode($lineEnding, $lines).$lineEnding; + } + + protected function detectLineEnding(string $content): string + { + if (str_contains($content, "\r\n")) { + return "\r\n"; + } + + if (str_contains($content, "\r")) { + return "\r"; + } + + return "\n"; + } + + /** + * @return list + */ + protected function gitignoreLines(string $content): array + { + $trimmedContent = rtrim($content, "\r\n"); + + if ($trimmedContent === '') { + return []; + } + + return preg_split('/\r\n|\n|\r/', $trimmedContent) ?: []; + } + protected function outro(): void { $url = 'https://laravel.com/docs/boost'; diff --git a/tests/Feature/Console/InstallCommandGitignoreTest.php b/tests/Feature/Console/InstallCommandGitignoreTest.php new file mode 100644 index 00000000..b82f65ee --- /dev/null +++ b/tests/Feature/Console/InstallCommandGitignoreTest.php @@ -0,0 +1,106 @@ +originalBasePath = app()->basePath(); + $this->sandboxBasePath = sys_get_temp_dir().'/boost-install-command-'.Str::uuid(); + + File::ensureDirectoryExists($this->sandboxBasePath); + + app()->setBasePath($this->sandboxBasePath); + + $this->app->instance(InstallCommand::class, new class(app(AgentsDetector::class), app(Cloud::class), app(Config::class), app(Nightwatch::class), app(Sail::class), app(Terminal::class)) extends InstallCommand + { + protected function displayBoostHeader(string $featureName, string $projectName, ?Theme $theme = null): void {} + + protected function discoverEnvironment(): void {} + + protected function collectInstallationPreferences(): void {} + + protected function performInstallation(): void {} + + protected function outro(): void {} + }); +}); + +afterEach(function (): void { + app()->setBasePath($this->originalBasePath); + File::deleteDirectory($this->sandboxBasePath); +}); + +it('appends laravel boost ignore rules to an existing gitignore', function (): void { + File::put(base_path('.gitignore'), "vendor/\nnode_modules/"); + + $this->artisan('boost:install') + ->expectsConfirmation('Would you like to add recommended AI artifacts to .gitignore?', 'yes') + ->expectsOutputToContain('Added Laravel Boost ignore rules to .gitignore.') + ->assertSuccessful(); + + expect(File::get(base_path('.gitignore')))->toBe(implode("\n", [ + 'vendor/', + 'node_modules/', + '', + '# Laravel Boost', + '.ai/generated', + '.ai/cache', + '.claude/', + '.cursor/rules/generated', + '', + ])); +}); + +it('creates a gitignore file when one does not exist', function (): void { + $this->artisan('boost:install') + ->expectsConfirmation('Would you like to add recommended AI artifacts to .gitignore?', 'yes') + ->expectsOutputToContain('Added Laravel Boost ignore rules to .gitignore.') + ->assertSuccessful(); + + expect(File::get(base_path('.gitignore')))->toBe(implode("\n", [ + '# Laravel Boost', + '.ai/generated', + '.ai/cache', + '.claude/', + '.cursor/rules/generated', + '', + ])); +}); + +it('avoids duplicating existing entries and only merges missing rules into the boost section', function (): void { + File::put(base_path('.gitignore'), implode("\n", [ + 'vendor/', + '# Laravel Boost', + '.ai/generated', + '.claude/', + '', + '.cursor/rules/generated', + '', + ])); + + $this->artisan('boost:install') + ->expectsConfirmation('Would you like to add recommended AI artifacts to .gitignore?', 'yes') + ->expectsOutputToContain('Added Laravel Boost ignore rules to .gitignore.') + ->assertSuccessful(); + + expect(File::get(base_path('.gitignore')))->toBe(implode("\n", [ + 'vendor/', + '# Laravel Boost', + '.ai/generated', + '.claude/', + '.ai/cache', + '', + '.cursor/rules/generated', + '', + ])); +}); From 295e20a711091b7fed7ffe2d39df006553c7e677 Mon Sep 17 00:00:00 2001 From: Sagar Naliyapara Date: Mon, 25 May 2026 13:17:51 +0530 Subject: [PATCH 2/2] Show suggested Boost gitignore entry after install --- src/Console/InstallCommand.php | 97 ++----------------- .../Console/InstallCommandGitignoreTest.php | 80 ++------------- 2 files changed, 14 insertions(+), 163 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index eeee41fb..3186e7ef 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -88,7 +88,7 @@ public function handle(): int $this->discoverEnvironment(); $this->collectInstallationPreferences(); $this->performInstallation(); - $this->updateGitignore(); + $this->displayGitignoreSuggestions(); $this->outro(); return self::SUCCESS; @@ -141,101 +141,16 @@ protected function performInstallation(): void $this->storeConfig(); } - protected function updateGitignore(): void + protected function displayGitignoreSuggestions(): void { if (! $this->input->isInteractive()) { - $this->info('Skipped .gitignore updates.'); - - return; - } - - if (! $this->confirm('Would you like to add recommended AI artifacts to .gitignore?')) { - $this->info('Skipped .gitignore updates.'); - return; } - $path = base_path('.gitignore'); - $content = file_exists($path) ? file_get_contents($path) : ''; - $updatedContent = $this->mergeBoostGitignoreRules($content ?: ''); - - if ($updatedContent === $content) { - $this->info('Laravel Boost ignore rules are already present in .gitignore.'); - - return; - } - - file_put_contents($path, $updatedContent); - - $this->info('Added Laravel Boost ignore rules to .gitignore.'); - } - - protected function mergeBoostGitignoreRules(string $content): string - { - $lineEnding = $this->detectLineEnding($content); - $lines = $this->gitignoreLines($content); - $normalizedLines = collect($lines)->map(fn (string $line): string => trim($line)); - $entries = collect([ - '.ai/generated', - '.ai/cache', - '.claude/', - '.cursor/rules/generated', - ]); - - $missingEntries = $entries - ->reject(fn (string $entry): bool => $normalizedLines->contains($entry)) - ->values(); - - if ($missingEntries->isEmpty()) { - return $content; - } - - if ($normalizedLines->contains('# Laravel Boost')) { - $sectionStart = $normalizedLines->search('# Laravel Boost'); - $sectionEnd = $sectionStart + 1; - - while (isset($lines[$sectionEnd]) && $entries->contains(trim($lines[$sectionEnd]))) { - $sectionEnd++; - } - - array_splice($lines, $sectionEnd, 0, $missingEntries->all()); - } else { - $lines = [ - ...$lines, - ...($lines === [] ? [] : ['']), - '# Laravel Boost', - ...$missingEntries->all(), - ]; - } - - return implode($lineEnding, $lines).$lineEnding; - } - - protected function detectLineEnding(string $content): string - { - if (str_contains($content, "\r\n")) { - return "\r\n"; - } - - if (str_contains($content, "\r")) { - return "\r"; - } - - return "\n"; - } - - /** - * @return list - */ - protected function gitignoreLines(string $content): array - { - $trimmedContent = rtrim($content, "\r\n"); - - if ($trimmedContent === '') { - return []; - } - - return preg_split('/\r\n|\n|\r/', $trimmedContent) ?: []; + $this->newLine(); + $this->info('Suggested .gitignore entry for Boost:'); + $this->line('# Laravel Boost'); + $this->line('boost.json'); } protected function outro(): void diff --git a/tests/Feature/Console/InstallCommandGitignoreTest.php b/tests/Feature/Console/InstallCommandGitignoreTest.php index b82f65ee..28fbdbbe 100644 --- a/tests/Feature/Console/InstallCommandGitignoreTest.php +++ b/tests/Feature/Console/InstallCommandGitignoreTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -use Illuminate\Support\Facades\File; -use Illuminate\Support\Str; use Laravel\Boost\Console\Enums\Theme; use Laravel\Boost\Console\InstallCommand; use Laravel\Boost\Install\AgentsDetector; @@ -14,13 +12,6 @@ use Laravel\Prompts\Terminal; beforeEach(function (): void { - $this->originalBasePath = app()->basePath(); - $this->sandboxBasePath = sys_get_temp_dir().'/boost-install-command-'.Str::uuid(); - - File::ensureDirectoryExists($this->sandboxBasePath); - - app()->setBasePath($this->sandboxBasePath); - $this->app->instance(InstallCommand::class, new class(app(AgentsDetector::class), app(Cloud::class), app(Config::class), app(Nightwatch::class), app(Sail::class), app(Terminal::class)) extends InstallCommand { protected function displayBoostHeader(string $featureName, string $projectName, ?Theme $theme = null): void {} @@ -35,72 +26,17 @@ protected function outro(): void {} }); }); -afterEach(function (): void { - app()->setBasePath($this->originalBasePath); - File::deleteDirectory($this->sandboxBasePath); -}); - -it('appends laravel boost ignore rules to an existing gitignore', function (): void { - File::put(base_path('.gitignore'), "vendor/\nnode_modules/"); - +it('shows suggested gitignore entries in interactive installs', function (): void { $this->artisan('boost:install') - ->expectsConfirmation('Would you like to add recommended AI artifacts to .gitignore?', 'yes') - ->expectsOutputToContain('Added Laravel Boost ignore rules to .gitignore.') + ->expectsOutputToContain('Suggested .gitignore entry for Boost:') + ->expectsOutputToContain('# Laravel Boost') + ->expectsOutputToContain('boost.json') ->assertSuccessful(); - - expect(File::get(base_path('.gitignore')))->toBe(implode("\n", [ - 'vendor/', - 'node_modules/', - '', - '# Laravel Boost', - '.ai/generated', - '.ai/cache', - '.claude/', - '.cursor/rules/generated', - '', - ])); }); -it('creates a gitignore file when one does not exist', function (): void { - $this->artisan('boost:install') - ->expectsConfirmation('Would you like to add recommended AI artifacts to .gitignore?', 'yes') - ->expectsOutputToContain('Added Laravel Boost ignore rules to .gitignore.') +it('does not show suggested gitignore entries in non-interactive installs', function (): void { + $this->artisan('boost:install', ['--no-interaction' => true]) + ->doesntExpectOutputToContain('Suggested .gitignore entry for Boost:') + ->doesntExpectOutputToContain('boost.json') ->assertSuccessful(); - - expect(File::get(base_path('.gitignore')))->toBe(implode("\n", [ - '# Laravel Boost', - '.ai/generated', - '.ai/cache', - '.claude/', - '.cursor/rules/generated', - '', - ])); -}); - -it('avoids duplicating existing entries and only merges missing rules into the boost section', function (): void { - File::put(base_path('.gitignore'), implode("\n", [ - 'vendor/', - '# Laravel Boost', - '.ai/generated', - '.claude/', - '', - '.cursor/rules/generated', - '', - ])); - - $this->artisan('boost:install') - ->expectsConfirmation('Would you like to add recommended AI artifacts to .gitignore?', 'yes') - ->expectsOutputToContain('Added Laravel Boost ignore rules to .gitignore.') - ->assertSuccessful(); - - expect(File::get(base_path('.gitignore')))->toBe(implode("\n", [ - 'vendor/', - '# Laravel Boost', - '.ai/generated', - '.claude/', - '.ai/cache', - '', - '.cursor/rules/generated', - '', - ])); });