Skip to content

Commit 58f3fbb

Browse files
committed
feat: add confirmation before overwriting existing config files
Adds intelligent protection against accidentally overwriting custom configuration files with an interactive prompt and --force option. Changes: - Added --force option to bypass confirmation prompts - Modified copyStubFile() to check if destination file exists - Added interactive confirmation prompt when file exists (using Laravel Prompts) - Shows informative messages for user actions: - "Overwriting {file}..." when proceeding - "⚠️ Skipping {file} (file already exists)" when user declines - Added 4 comprehensive tests covering all scenarios: 1. Skip file when user declines 2. Overwrite when user confirms 3. Overwrite with --force (no prompt) 4. Copy normally when file doesn't exist Behavior: - Default: Asks confirmation if config file exists - With --force: Overwrites without asking - User can decline: Skips file with warning Example interaction: $ php artisan laravel-init:install File pint.json already exists. Overwrite? [yes/no] > no ⚠️ Skipping pint.json (file already exists) $ php artisan laravel-init:install --force Overwriting pint.json... ✅ Pint installed successfully Benefits: - Protects custom configurations from accidental overwrites - Provides clear user feedback - Offers --force for automation scenarios - All tests passing (33 passed, 75 assertions) This ensures users don't lose their carefully crafted configuration files.
1 parent 27addf3 commit 58f3fbb

2 files changed

Lines changed: 131 additions & 1 deletion

File tree

src/Commands/InstallCommand.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
use Illuminate\Support\Facades\File;
99
use Illuminate\Support\Facades\Process;
1010

11+
use function Laravel\Prompts\confirm;
1112
use function Laravel\Prompts\spin;
1213

1314
class InstallCommand extends Command
1415
{
15-
public $signature = 'laravel-init:install';
16+
public $signature = 'laravel-init:install {--force : Overwrite existing configuration files without confirmation}';
1617

1718
public $description = 'Install Pint, PhpStan, Pest, Pail.';
1819

@@ -109,12 +110,33 @@ protected function isPackageInstalled(string $packageName): bool
109110

110111
/**
111112
* Copy a stub configuration file to the project root.
113+
* Asks for confirmation if the file already exists (unless --force is used).
112114
*/
113115
protected function copyStubFile(string $stubFileName, string $destinationFileName): void
114116
{
115117
$source = __DIR__."/../../stubs/{$stubFileName}";
116118
$destination = base_path($destinationFileName);
117119

120+
// Check if file already exists
121+
if (File::exists($destination)) {
122+
// If --force option is not set, ask for confirmation
123+
if (! $this->option('force')) {
124+
$shouldOverwrite = confirm(
125+
label: "File {$destinationFileName} already exists. Overwrite?",
126+
default: false
127+
);
128+
129+
if (! $shouldOverwrite) {
130+
$this->warn("⚠️ Skipping {$destinationFileName} (file already exists)");
131+
132+
return;
133+
}
134+
}
135+
136+
// User confirmed or --force is set
137+
$this->info("Overwriting {$destinationFileName}...");
138+
}
139+
118140
$result = File::copy($source, $destination);
119141

120142
if (! $result) {

tests/Feature/InstallCommandTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,111 @@
567567
->expectsOutput('"refactor": "vendor/bin/rector"');
568568

569569
});
570+
571+
it('skips existing config file when user declines to overwrite', function (): void {
572+
// Arrange
573+
File::shouldReceive('exists')
574+
->with(base_path('composer.json'))
575+
->andReturnTrue();
576+
File::shouldReceive('get')
577+
->with(base_path('composer.json'))
578+
->andReturn(json_encode(['require' => [], 'require-dev' => []]));
579+
File::shouldReceive('exists')
580+
->with(base_path('pint.json'))
581+
->andReturnTrue(); // pint.json already exists
582+
File::shouldReceive('exists')
583+
->with(base_path('phpstan.neon.dist'))
584+
->andReturnFalse();
585+
File::shouldReceive('exists')
586+
->with(base_path('rector.php'))
587+
->andReturnFalse();
588+
File::shouldReceive('copy')
589+
->with(Mockery::type('string'), base_path('phpstan.neon.dist'))
590+
->andReturnTrue();
591+
File::shouldReceive('copy')
592+
->with(Mockery::type('string'), base_path('rector.php'))
593+
->andReturnTrue();
594+
// pint.json should NOT be copied
595+
596+
Process::fake();
597+
598+
// Act & Assert
599+
$this->artisan('laravel-init:install')
600+
->expectsConfirmation('File pint.json already exists. Overwrite?', 'no')
601+
->expectsOutputToContain('Skipping pint.json')
602+
->assertExitCode(0);
603+
});
604+
605+
it('overwrites existing config file when user confirms', function (): void {
606+
// Arrange
607+
File::shouldReceive('exists')
608+
->with(base_path('composer.json'))
609+
->andReturnTrue();
610+
File::shouldReceive('get')
611+
->with(base_path('composer.json'))
612+
->andReturn(json_encode(['require' => [], 'require-dev' => []]));
613+
File::shouldReceive('exists')
614+
->with(base_path('pint.json'))
615+
->andReturnTrue(); // pint.json already exists
616+
File::shouldReceive('exists')
617+
->with(base_path('phpstan.neon.dist'))
618+
->andReturnFalse();
619+
File::shouldReceive('exists')
620+
->with(base_path('rector.php'))
621+
->andReturnFalse();
622+
File::shouldReceive('copy')
623+
->andReturnTrue(); // All copies succeed
624+
625+
Process::fake();
626+
627+
// Act & Assert
628+
$this->artisan('laravel-init:install')
629+
->expectsConfirmation('File pint.json already exists. Overwrite?', 'yes')
630+
->expectsOutputToContain('Overwriting pint.json')
631+
->assertExitCode(0);
632+
});
633+
634+
it('overwrites existing config file with --force without asking', function (): void {
635+
// Arrange
636+
File::shouldReceive('exists')
637+
->with(base_path('composer.json'))
638+
->andReturnTrue();
639+
File::shouldReceive('get')
640+
->with(base_path('composer.json'))
641+
->andReturn(json_encode(['require' => [], 'require-dev' => []]));
642+
File::shouldReceive('exists')
643+
->with(base_path('pint.json'))
644+
->andReturnTrue(); // pint.json already exists
645+
File::shouldReceive('exists')
646+
->with(base_path('phpstan.neon.dist'))
647+
->andReturnFalse();
648+
File::shouldReceive('exists')
649+
->with(base_path('rector.php'))
650+
->andReturnFalse();
651+
File::shouldReceive('copy')
652+
->andReturnTrue(); // Should copy with --force
653+
654+
Process::fake();
655+
656+
// Act & Assert
657+
$this->artisan('laravel-init:install --force')
658+
->expectsOutputToContain('Overwriting pint.json')
659+
->assertExitCode(0);
660+
});
661+
662+
it('copies new config file without prompting when file does not exist', function (): void {
663+
// Arrange
664+
File::shouldReceive('exists')
665+
->andReturnFalse(); // No files exist
666+
File::shouldReceive('copy')
667+
->andReturnTrue();
668+
File::shouldReceive('get')
669+
->andReturn(json_encode(['require' => [], 'require-dev' => []]));
670+
671+
Process::fake();
672+
673+
// Act & Assert
674+
$this->artisan('laravel-init:install')
675+
->doesntExpectOutput('already exists') // No prompts since files don't exist
676+
->assertExitCode(0);
677+
});

0 commit comments

Comments
 (0)