Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,49 @@ JSON Example:
}
```

## Adding Support for Other IDEs / AI Agents

Boost works with many popular IDEs and AI agents out of the box. If your coding tool isn't supported yet, you can create your own code environment and integrate it with Boost. To do this, create a class that extends `Laravel\Boost\Install\CodeEnvironment\CodeEnvironment` and implement one or both of the following contracts depending on what you need:

- `Laravel\Boost\Contracts\Agent` - Adds support for AI guidelines.
- `Laravel\Boost\Contracts\McpClient` - Adds support for MCP.

### Writing the Code Environment

```php
<?php

declare(strict_types=1);

namespace App;

use Laravel\Boost\Contracts\Agent;
use Laravel\Boost\Contracts\McpClient;
use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment;

class OpenCode extends CodeEnvironment implements Agent, McpClient
{
// Your implementation...
}
```

For an example implementation, see [ClaudeCode.php](https://github.com/laravel/boost/blob/main/src/Install/CodeEnvironment/ClaudeCode.php).

### Registering the Code Environment

Register your custom code environment in the `boot` method of your application's `App\Providers\AppServiceProvider`:

```php
use Laravel\Boost\Boost;

public function boot(): void
{
Boost::registerCodeEnvironment('opencode', OpenCode::class);
}
```

Once registered, your code environment will be available for selection when running `php artisan boost:install`.

## Contributing

Thank you for considering contributing to Boost! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
Expand Down
21 changes: 21 additions & 0 deletions src/Boost.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Laravel\Boost;

use Illuminate\Support\Facades\Facade;

/**
* @method static void registerCodeEnvironment(string $key, string $className)
* @method static array getCodeEnvironments()
*
* @see \Laravel\Boost\BoostManager
*/
class Boost extends Facade
{
protected static function getFacadeAccessor(): string
{
return BoostManager::class;
}
}
47 changes: 47 additions & 0 deletions src/BoostManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Laravel\Boost;

use InvalidArgumentException;
use Laravel\Boost\Install\CodeEnvironment\ClaudeCode;
use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment;
use Laravel\Boost\Install\CodeEnvironment\Codex;
use Laravel\Boost\Install\CodeEnvironment\Copilot;
use Laravel\Boost\Install\CodeEnvironment\Cursor;
use Laravel\Boost\Install\CodeEnvironment\PhpStorm;
use Laravel\Boost\Install\CodeEnvironment\VSCode;

class BoostManager
{
/** @var array<string, class-string<CodeEnvironment>> */
private array $codeEnvironments = [
'phpstorm' => PhpStorm::class,
'vscode' => VSCode::class,
'cursor' => Cursor::class,
'claudecode' => ClaudeCode::class,
'codex' => Codex::class,
'copilot' => Copilot::class,
];

/**
* @param class-string<CodeEnvironment> $className
*/
public function registerCodeEnvironment(string $key, string $className): void
{
if (array_key_exists($key, $this->codeEnvironments)) {
throw new InvalidArgumentException("Code environment '{$key}' is already registered");
}

$this->codeEnvironments[$key] = $className;
}

/**
* @return array<string, class-string<CodeEnvironment>>
*/
public function getCodeEnvironments(): array
{
return $this->codeEnvironments;
}
}
2 changes: 2 additions & 0 deletions src/BoostServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public function register(): void
return;
}

$this->app->singleton(BoostManager::class, fn (): BoostManager => new BoostManager);

$this->app->singleton(Roster::class, function () {
$lockFiles = [
base_path('composer.lock'),
Expand Down
16 changes: 8 additions & 8 deletions src/Console/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ protected function selectBoostFeatures(): Collection
protected function selectAiGuidelines(): Collection
{
$options = app(GuidelineComposer::class)->guidelines()
->reject(fn (array $guideline) => $guideline['third_party'] === false);
->reject(fn (array $guideline): bool => $guideline['third_party'] === false);

if ($options->isEmpty()) {
return collect();
Expand All @@ -270,7 +270,7 @@ protected function selectAiGuidelines(): Collection
return collect(multiselect(
label: 'Which third-party AI guidelines do you want to install?',
// @phpstan-ignore-next-line
options: $options->mapWithKeys(function (array $guideline, string $name) {
options: $options->mapWithKeys(function (array $guideline, string $name): array {
$humanName = str_replace('/core', '', $name);

return [$name => "{$humanName} (~{$guideline['tokens']} tokens) {$guideline['description']}"];
Expand Down Expand Up @@ -308,13 +308,13 @@ protected function selectTargetAgents(): Collection
/**
* Get configuration settings for contract-specific selection behavior.
*
* @return array{scroll: int, required: bool, displayMethod: string}
* @return array{required: bool, displayMethod: string}
*/
protected function getSelectionConfig(string $contractClass): array
{
return match ($contractClass) {
Agent::class => ['scroll' => 5, 'required' => false, 'displayMethod' => 'agentName'],
McpClient::class => ['scroll' => 5, 'required' => true, 'displayMethod' => 'displayName'],
Agent::class => ['required' => false, 'displayMethod' => 'agentName'],
McpClient::class => ['required' => true, 'displayMethod' => 'displayName'],
default => throw new InvalidArgumentException("Unsupported contract class: {$contractClass}"),
};
}
Expand Down Expand Up @@ -361,7 +361,7 @@ protected function selectCodeEnvironments(string $contractClass, string $label,
label: $label,
options: $options->toArray(),
default: $defaults === [] ? $detectedDefaults : $defaults,
scroll: $config['scroll'],
scroll: $options->count(),
required: $config['required'],
hint: $defaults === [] || $detectedDefaults === [] ? '' : sprintf('Auto-detected %s for you',
Arr::join(array_map(function ($className) use ($availableEnvironments, $config) {
Expand Down Expand Up @@ -447,11 +447,11 @@ protected function installGuidelines(): void
);

$this->config->setEditors(
$this->selectedTargetMcpClient->map(fn (McpClient $mcpClient) => $mcpClient->name())->values()->toArray()
$this->selectedTargetMcpClient->map(fn (McpClient $mcpClient): string => $mcpClient->name())->values()->toArray()
);

$this->config->setAgents(
$this->selectedTargetAgents->map(fn (Agent $agent) => $agent->name())->values()->toArray()
$this->selectedTargetAgents->map(fn (Agent $agent): string => $agent->name())->values()->toArray()
);

$this->config->setGuidelines(
Expand Down
20 changes: 5 additions & 15 deletions src/Install/CodeEnvironment/CodeEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

namespace Laravel\Boost\Install\CodeEnvironment;

use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Facades\Process;
use Laravel\Boost\BoostManager;
use Laravel\Boost\Contracts\Agent;
use Laravel\Boost\Contracts\McpClient;
use Laravel\Boost\Install\Detection\DetectionStrategyFactory;
Expand Down Expand Up @@ -93,19 +93,13 @@ public function mcpInstallationStrategy(): McpInstallationStrategy
return McpInstallationStrategy::FILE;
}

public static function fromName(string $name): ?static
public static function fromName(string $name): ?CodeEnvironment
{
$detectionFactory = app(DetectionStrategyFactory::class);
$boostManager = app(BoostManager::class);

foreach ([
ClaudeCode::class,
Codex::class,
Copilot::class,
Cursor::class,
PhpStorm::class,
VSCode::class,
] as $class) {
/** @var class-string<static> $class */
foreach ($boostManager->getCodeEnvironments() as $class) {
/** @var class-string<CodeEnvironment> $class */
$instance = new $class($detectionFactory);
if ($instance->name() === $name) {
return $instance;
Expand Down Expand Up @@ -140,8 +134,6 @@ public function mcpConfigKey(): string
*
* @param array<int, string> $args
* @param array<string, string> $env
*
* @throws FileNotFoundException
*/
public function installMcp(string $key, string $command, array $args = [], array $env = []): bool
{
Expand Down Expand Up @@ -198,8 +190,6 @@ protected function installShellMcp(string $key, string $command, array $args = [
*
* @param array<int, string> $args
* @param array<string, string> $env
*
* @throws FileNotFoundException
*/
protected function installFileMcp(string $key, string $command, array $args = [], array $env = []): bool
{
Expand Down
27 changes: 7 additions & 20 deletions src/Install/CodeEnvironmentsDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,15 @@

use Illuminate\Container\Container;
use Illuminate\Support\Collection;
use Laravel\Boost\Install\CodeEnvironment\ClaudeCode;
use Laravel\Boost\BoostManager;
use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment;
use Laravel\Boost\Install\CodeEnvironment\Codex;
use Laravel\Boost\Install\CodeEnvironment\Copilot;
use Laravel\Boost\Install\CodeEnvironment\Cursor;
use Laravel\Boost\Install\CodeEnvironment\PhpStorm;
use Laravel\Boost\Install\CodeEnvironment\VSCode;
use Laravel\Boost\Install\Enums\Platform;

class CodeEnvironmentsDetector
{
/** @var array<string, class-string<CodeEnvironment>> */
private array $programs = [
'phpstorm' => PhpStorm::class,
'vscode' => VSCode::class,
'cursor' => Cursor::class,
'claudecode' => ClaudeCode::class,
'codex' => Codex::class,
'copilot' => Copilot::class,
];

public function __construct(
private readonly Container $container
private readonly Container $container,
private readonly BoostManager $boostManager
) {}

/**
Expand All @@ -55,8 +41,8 @@ public function discoverSystemInstalledCodeEnvironments(): array
public function discoverProjectInstalledCodeEnvironments(string $basePath): array
{
return $this->getCodeEnvironments()
->filter(fn ($program): bool => $program->detectInProject($basePath))
->map(fn ($program): string => $program->name())
->filter(fn (CodeEnvironment $program): bool => $program->detectInProject($basePath))
->map(fn (CodeEnvironment $program): string => $program->name())
->values()
->toArray();
}
Expand All @@ -68,6 +54,7 @@ public function discoverProjectInstalledCodeEnvironments(string $basePath): arra
*/
public function getCodeEnvironments(): Collection
{
return collect($this->programs)->map(fn (string $className) => $this->container->make($className));
return collect($this->boostManager->getCodeEnvironments())
->map(fn (string $className) => $this->container->make($className));
}
}
10 changes: 5 additions & 5 deletions src/Support/Composer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ class Composer
public static function packagesDirectories(): array
{
return collect(static::packages())
->mapWithKeys(fn (string $key, string $package) => [$package => implode(DIRECTORY_SEPARATOR, [
->mapWithKeys(fn (string $key, string $package): array => [$package => implode(DIRECTORY_SEPARATOR, [
base_path('vendor'),
str_replace('/', DIRECTORY_SEPARATOR, $package),
])])
->filter(fn (string $path) => is_dir($path))
->filter(fn (string $path): bool => is_dir($path))
->toArray();
}

Expand All @@ -33,19 +33,19 @@ public static function packages(): array

return collect($composerData['require'] ?? [])
->merge($composerData['require-dev'] ?? [])
->mapWithKeys(fn (string $key, string $package) => [$package => $key])
->mapWithKeys(fn (string $key, string $package): array => [$package => $key])
->toArray();
}

public static function packagesDirectoriesWithBoostGuidelines(): array
{
return collect(Composer::packagesDirectories())
->map(fn (string $path) => implode(DIRECTORY_SEPARATOR, [
->map(fn (string $path): string => implode(DIRECTORY_SEPARATOR, [
$path,
'resources',
'boost',
'guidelines',
]))->filter(fn (string $path) => is_dir($path))
]))->filter(fn (string $path): bool => is_dir($path))
->toArray();
}
}
34 changes: 34 additions & 0 deletions tests/Feature/BoostFacadeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

use Laravel\Boost\Boost;
use Laravel\Boost\BoostManager;
use Tests\Unit\Install\ExampleCodeEnvironment;

it('Boost Facade resolves to BoostManager instance', function (): void {
$instance = Boost::getFacadeRoot();

expect($instance)->toBeInstanceOf(BoostManager::class);
});

it('Boost Facade registers code environments via facade', function (): void {
Boost::registerCodeEnvironment('example1', ExampleCodeEnvironment::class);
Boost::registerCodeEnvironment('example2', ExampleCodeEnvironment::class);
$registered = Boost::getFacadeRoot()->getCodeEnvironments();

expect($registered)->toHaveKey('example1')
->and($registered['example1'])->toBe(ExampleCodeEnvironment::class)
->and($registered)->toHaveKey('example2')
->and($registered['example2'])->toBe(ExampleCodeEnvironment::class)
->and($registered)->toHaveKey('phpstorm');
});

it('Boost Facade maintains registration state across facade calls', function (): void {
Boost::registerCodeEnvironment('persistent', 'Test\Persistent');

$registered = Boost::getFacadeRoot()->getCodeEnvironments();

expect($registered)->toHaveKey('persistent')
->and($registered['persistent'])->toBe('Test\Persistent');
});
Loading