Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/BoostManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Laravel\Boost\Install\CodeEnvironment\Codex;
use Laravel\Boost\Install\CodeEnvironment\Copilot;
use Laravel\Boost\Install\CodeEnvironment\Cursor;
use Laravel\Boost\Install\CodeEnvironment\OpenCode;
use Laravel\Boost\Install\CodeEnvironment\PhpStorm;
use Laravel\Boost\Install\CodeEnvironment\VSCode;

Expand All @@ -23,6 +24,7 @@ class BoostManager
'claudecode' => ClaudeCode::class,
'codex' => Codex::class,
'copilot' => Copilot::class,
'opencode' => OpenCode::class,
];

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Install/CodeEnvironment/ClaudeCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function systemDetectionConfig(Platform $platform): array
{
return match ($platform) {
Platform::Darwin, Platform::Linux => [
'command' => 'which claude',
'command' => 'command -v claude',
],
Platform::Windows => [
'command' => 'where claude 2>nul',
Expand Down
26 changes: 24 additions & 2 deletions src/Install/CodeEnvironment/CodeEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ public function mcpConfigKey(): string
return 'mcpServers';
}

/** @return array<string, mixed> */
public function defaultMcpConfig(): array
{
return [];
}

/**
* Install MCP server using the appropriate strategy.
*
Expand All @@ -144,6 +150,22 @@ public function installMcp(string $key, string $command, array $args = [], array
};
}

/**
* Build the MCP server configuration payload for file-based installation.
*
* @param array<int, string> $args
* @param array<string, string> $env
* @return array<string, mixed>
*/
public function mcpServerConfig(string $command, array $args = [], array $env = []): array
{
return [
'command' => $command,
'args' => $args,
'env' => $env,
];
}

/**
* Install MCP server using a shell command strategy.
*
Expand Down Expand Up @@ -198,9 +220,9 @@ protected function installFileMcp(string $key, string $command, array $args = []
return false;
}

return (new FileWriter($path))
return (new FileWriter($path, $this->defaultMcpConfig()))
->configKey($this->mcpConfigKey())
->addServer($key, $command, $args, $env)
->addServerConfig($key, $this->mcpServerConfig($command, $args, $env))
->save();
}
}
81 changes: 81 additions & 0 deletions src/Install/CodeEnvironment/OpenCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Laravel\Boost\Install\CodeEnvironment;

use Laravel\Boost\Contracts\Agent;
use Laravel\Boost\Contracts\McpClient;
use Laravel\Boost\Install\Enums\McpInstallationStrategy;
use Laravel\Boost\Install\Enums\Platform;

class OpenCode extends CodeEnvironment implements Agent, McpClient
{
public function name(): string
{
return 'opencode';
}

public function displayName(): string
{
return 'OpenCode';
}

public function systemDetectionConfig(Platform $platform): array
{
return match ($platform) {
Platform::Darwin, Platform::Linux => [
'command' => 'command -v opencode',
],
Platform::Windows => [
'command' => 'where opencode 2>nul',
],
};
}

public function projectDetectionConfig(): array
{
return [
'files' => ['AGENTS.md', 'opencode.json'],
];
}

public function mcpInstallationStrategy(): McpInstallationStrategy
{
return McpInstallationStrategy::FILE;
}

public function mcpConfigPath(): string
{
return 'opencode.json';
}

public function guidelinesPath(): string
{
return 'AGENTS.md';
}

public function mcpConfigKey(): string
{
return 'mcp';
}

/** {@inheritDoc} */
public function defaultMcpConfig(): array
{
return [
'$schema' => 'https://opencode.ai/config.json',
];
}

/** {@inheritDoc} */
public function mcpServerConfig(string $command, array $args = [], array $env = []): array
{
return [
'type' => 'local',
'enabled' => true,
'command' => [$command, ...$args],
'environment' => $env,
];
}
}
2 changes: 1 addition & 1 deletion src/Install/CodeEnvironment/VSCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function systemDetectionConfig(Platform $platform): array
'paths' => ['/Applications/Visual Studio Code.app'],
],
Platform::Linux => [
'command' => 'which code',
'command' => 'command -v code',
],
Platform::Windows => [
'paths' => [
Expand Down
21 changes: 16 additions & 5 deletions src/Install/Mcp/FileWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class FileWriter

protected int $defaultIndentation = 8;

public function __construct(protected string $filePath) {}
public function __construct(protected string $filePath, protected array $baseConfig = []) {}

public function configKey(string $key): self
{
Expand All @@ -25,17 +25,28 @@ public function configKey(string $key): self
}

/**
* @param string $key MCP Server Name
* @deprecated Use addServerConfig() for array-based configuration.
*
* @param array<int, string> $args
* @param array<string, string> $env
*/
public function addServer(string $key, string $command, array $args = [], array $env = []): self
{
$this->serversToAdd[$key] = collect([
return $this->addServerConfig($key, collect([
'command' => $command,
'args' => $args,
'env' => $env,
])->filter()->toArray();
])->filter(fn ($value): bool => ! in_array($value, [[], null, ''], true))->toArray());
}

/**
* @param array<string, mixed> $config
*/
public function addServerConfig(string $key, array $config): self
{
$this->serversToAdd[$key] = collect($config)
->filter(fn ($value): bool => ! in_array($value, [[], null, ''], true))
->toArray();

return $this;
}
Expand Down Expand Up @@ -358,7 +369,7 @@ protected function hasUnquotedComments(string $content): bool

protected function createNewFile(): bool
{
$config = [];
$config = $this->baseConfig;
$this->addServersToConfig($config);

return $this->writeJsonConfig($config);
Expand Down
9 changes: 7 additions & 2 deletions tests/Unit/Install/CodeEnvironmentsDetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Laravel\Boost\Install\CodeEnvironment\Codex;
use Laravel\Boost\Install\CodeEnvironment\Copilot;
use Laravel\Boost\Install\CodeEnvironment\Cursor;
use Laravel\Boost\Install\CodeEnvironment\OpenCode;
use Laravel\Boost\Install\CodeEnvironment\PhpStorm;
use Laravel\Boost\Install\CodeEnvironment\VSCode;
use Laravel\Boost\Install\CodeEnvironmentsDetector;
Expand All @@ -29,9 +30,9 @@
$codeEnvironments = $this->detector->getCodeEnvironments();

expect($codeEnvironments)->toBeInstanceOf(Collection::class)
->and($codeEnvironments->count())->toBe(6)
->and($codeEnvironments->count())->toBe(7)
->and($codeEnvironments->keys()->toArray())->toBe([
'phpstorm', 'vscode', 'cursor', 'claudecode', 'codex', 'copilot',
'phpstorm', 'vscode', 'cursor', 'claudecode', 'codex', 'copilot', 'opencode',
]);

$codeEnvironments->each(function ($environment): void {
Expand Down Expand Up @@ -62,6 +63,7 @@
$this->container->bind(ClaudeCode::class, fn () => $mockOther);
$this->container->bind(Codex::class, fn () => $mockOther);
$this->container->bind(Copilot::class, fn () => $mockOther);
$this->container->bind(OpenCode::class, fn () => $mockOther);

$detector = new CodeEnvironmentsDetector($this->container, $this->boostManager);
$detected = $detector->discoverSystemInstalledCodeEnvironments();
Expand All @@ -80,6 +82,7 @@
$this->container->bind(ClaudeCode::class, fn () => $mockEnvironment);
$this->container->bind(Codex::class, fn () => $mockEnvironment);
$this->container->bind(Copilot::class, fn () => $mockEnvironment);
$this->container->bind(OpenCode::class, fn () => $mockEnvironment);

$detector = new CodeEnvironmentsDetector($this->container, $this->boostManager);
$detected = $detector->discoverSystemInstalledCodeEnvironments();
Expand Down Expand Up @@ -112,6 +115,7 @@
$this->container->bind(ClaudeCode::class, fn () => $mockClaudeCode);
$this->container->bind(Codex::class, fn () => $mockOther);
$this->container->bind(Copilot::class, fn () => $mockOther);
$this->container->bind(OpenCode::class, fn () => $mockOther);

$detector = new CodeEnvironmentsDetector($this->container, $this->boostManager);
$detected = $detector->discoverProjectInstalledCodeEnvironments($basePath);
Expand All @@ -132,6 +136,7 @@
$this->container->bind(ClaudeCode::class, fn () => $mockEnvironment);
$this->container->bind(Codex::class, fn () => $mockEnvironment);
$this->container->bind(Copilot::class, fn () => $mockEnvironment);
$this->container->bind(OpenCode::class, fn () => $mockEnvironment);

$detector = new CodeEnvironmentsDetector($this->container, $this->boostManager);
$detected = $detector->discoverProjectInstalledCodeEnvironments($basePath);
Expand Down
Loading