From d91170564572feb52d80a120439b12b750921829 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 19:30:20 +0530 Subject: [PATCH 01/16] refactor: simplify InstallCommand by consolidating CodeEnvironment handling and removing legacy agents - Removed `Agents` directory and deprecated agent-related classes. - Consolidated IDE and agent selection logic into `CodeEnvironment`. - Implemented MCP installation strategies in `CodeEnvironment`. - Added `McpInstallationStrategy` enum for shell, file, or none-based configurations. - Updated `InstallCommand` to use consolidated `CodeEnvironment` functionality. - Enhanced `CodeEnvironment` with support for MCP installation and guideline paths. --- src/Console/InstallCommand.php | 174 ++++++---------- src/Install/Agents/ClaudeCode.php | 22 --- src/Install/Agents/Copilot.php | 25 --- src/Install/Agents/Cursor.php | 25 --- src/Install/Agents/FileMcpIde.php | 68 ------- src/Install/Agents/Junie.php | 20 -- src/Install/Agents/PhpStormJunie.php | 13 -- src/Install/Agents/ShellMcpIde.php | 43 ---- src/Install/Agents/VsCode.php | 15 -- src/Install/Agents/Windsurf.php | 20 -- src/Install/CodeEnvironment/ClaudeCode.php | 25 ++- .../CodeEnvironment/CodeEnvironment.php | 187 +++++++++++++++++- src/Install/CodeEnvironment/Copilot.php | 17 +- src/Install/CodeEnvironment/Cursor.php | 24 ++- src/Install/CodeEnvironment/PhpStorm.php | 29 ++- src/Install/CodeEnvironment/VSCode.php | 23 ++- src/Install/CodeEnvironment/Zed.php | 18 +- src/Install/CodeEnvironmentsDetector.php | 10 + src/Install/Enums/McpInstallationStrategy.php | 12 ++ 19 files changed, 396 insertions(+), 374 deletions(-) delete mode 100644 src/Install/Agents/ClaudeCode.php delete mode 100644 src/Install/Agents/Copilot.php delete mode 100644 src/Install/Agents/Cursor.php delete mode 100644 src/Install/Agents/FileMcpIde.php delete mode 100644 src/Install/Agents/Junie.php delete mode 100644 src/Install/Agents/PhpStormJunie.php delete mode 100644 src/Install/Agents/ShellMcpIde.php delete mode 100644 src/Install/Agents/VsCode.php delete mode 100644 src/Install/Agents/Windsurf.php create mode 100644 src/Install/Enums/McpInstallationStrategy.php diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 5d19609..93a589f 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -9,9 +9,8 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\Ide; use Laravel\Boost\Install\Cli\DisplayHelper; +use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment; use Laravel\Boost\Install\CodeEnvironmentsDetector; use Laravel\Boost\Install\GuidelineComposer; use Laravel\Boost\Install\GuidelineConfig; @@ -19,7 +18,6 @@ use Laravel\Boost\Install\Herd; use Laravel\Prompts\Concerns\Colors; use Laravel\Prompts\Terminal; -use ReflectionClass; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Finder\Finder; @@ -39,10 +37,10 @@ class InstallCommand extends Command private Terminal $terminal; - /** @var Collection */ + /** @var Collection */ private Collection $selectedTargetAgents; - /** @var Collection */ + /** @var Collection */ private Collection $selectedTargetIdes; /** @var Collection */ @@ -57,8 +55,6 @@ class InstallCommand extends Command private bool $enforceTests = true; - private array $projectInstalledAgents = []; - private string $greenTick; private string $redCross; @@ -114,7 +110,6 @@ private function discoverEnvironment(): void { $this->systemInstalledCodeEnvironments = $this->codeEnvironmentsDetector->discoverSystemInstalledCodeEnvironments(); $this->projectInstalledCodeEnvironments = $this->codeEnvironmentsDetector->discoverProjectInstalledCodeEnvironments(base_path()); - $this->projectInstalledAgents = $this->discoverProjectAgents(); } private function collectInstallationPreferences(): void @@ -127,7 +122,7 @@ private function collectInstallationPreferences(): void private function enact(): void { - if ($this->shouldInstallAiGuidelines() && ! empty($this->selectedTargetAgents)) { + if ($this->shouldInstallAiGuidelines() && $this->selectedTargetAgents->isNotEmpty()) { $this->enactGuidelines(); } @@ -163,8 +158,8 @@ private function outro(): void { $label = 'https://boost.laravel.com/installed'; - $ideNames = $this->selectedTargetIdes->map(fn ($ide) => 'i:'.class_basename($ide))->toArray(); - $agentNames = $this->selectedTargetAgents->map(fn ($agent) => 'a:'.class_basename($agent))->toArray(); + $ideNames = $this->selectedTargetIdes->map(fn ($ide) => 'i:'.$ide->ideName())->toArray(); + $agentNames = $this->selectedTargetAgents->map(fn ($agent) => 'a:'.$agent->agentName())->toArray(); $boostFeatures = $this->selectedBoostFeatures->map(fn ($feature) => 'b:'.$feature)->toArray(); // Guidelines installed (prefix: g) @@ -261,137 +256,86 @@ protected function boostToolsToDisable(): array /** * @return array */ - private function discoverProjectAgents(): array - { - $agents = []; - $projectAgents = $this->codeEnvironmentsDetector->discoverProjectInstalledCodeEnvironments(base_path()); - - // Map IDE detections to their corresponding agents - $ideToAgentMap = [ - 'phpstorm' => 'junie', - 'claudecode' => 'claudecode', - 'cursor' => 'cursor', - 'copilot' => 'copilot', - ]; - - foreach ($projectAgents as $app) { - if (isset($ideToAgentMap[$app])) { - $agents[] = $ideToAgentMap[$app]; - } - } - - foreach ($this->systemInstalledCodeEnvironments as $ide) { - if (isset($ideToAgentMap[$ide]) && ! in_array($ideToAgentMap[$ide], $agents)) { - $agents[] = $ideToAgentMap[$ide]; - } - } - - return array_unique($agents); - } /** - * @return Collection + * @return Collection */ private function selectTargetIdes(): Collection { - $ides = []; if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { return collect(); } - $agentDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Install', 'Agents']); - - $finder = Finder::create() - ->in($agentDir) - ->files() - ->name('*.php'); - - foreach ($finder as $ideFile) { - $className = 'Laravel\\Boost\\Install\\Agents\\'.$ideFile->getBasename('.php'); - - if (class_exists($className)) { - $reflection = new ReflectionClass($className); - - if ($reflection->implementsInterface(Ide::class) && ! $reflection->isAbstract()) { - $ides[$className] = Str::headline($ideFile->getBasename('.php')); - } - } - } - - ksort($ides); - - $detectedClasses = []; - foreach ($this->projectInstalledCodeEnvironments as $ideKey) { - foreach ($ides as $className => $displayName) { - if (strtolower($ideKey) === strtolower(class_basename($className))) { - $detectedClasses[] = $className; - break; - } - } - } - - $selectedIdeClasses = collect(multiselect( - label: sprintf('Which code editors do you use in %s?', $this->projectName), - options: $ides, - default: $detectedClasses, - scroll: 5, - required: true, - hint: sprintf('Auto-detected %s for you', Arr::join(array_map(fn ($c) => class_basename($c), $detectedClasses), ', ', ' & ')) - ))->sort(); - - return $selectedIdeClasses->map(fn ($ideClass) => new $ideClass); + return $this->selectCodeEnvironments( + 'ide', + sprintf('Which code editors do you use in %s?', $this->projectName) + ); } /** - * @return Collection + * @return Collection */ private function selectTargetAgents(): Collection { - $agents = []; if (! $this->shouldInstallAiGuidelines()) { return collect(); } - $agentDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Install', 'Agents']); - - $finder = Finder::create() - ->in($agentDir) - ->files() - ->name('*.php'); + return $this->selectCodeEnvironments( + 'agent', + sprintf('Which agents need AI guidelines for %s?', $this->projectName) + ); + } - foreach ($finder as $agentFile) { - $className = 'Laravel\\Boost\\Install\\Agents\\'.$agentFile->getBasename('.php'); + /** + * @return Collection + */ + private function selectCodeEnvironments(string $type, string $label): Collection + { + $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); - if (class_exists($className)) { - $reflection = new ReflectionClass($className); + // Filter by type capability + $availableEnvironments = $allEnvironments->filter(function (CodeEnvironment $environment) use ($type) { + return ($type === 'ide' && $environment->supportsIde()) || + ($type === 'agent' && $environment->supportsAgent()); + }); - if ($reflection->implementsInterface(Agent::class)) { - $agents[$className] = Str::headline($agentFile->getBasename('.php')); - } - } + if ($availableEnvironments->isEmpty()) { + return collect(); } - ksort($agents); + // Build options for multiselect + $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) { + return [get_class($environment) => $environment->displayName()]; + })->sort(); - // Map detected agent keys to class names + // Auto-detect installed environments $detectedClasses = []; - foreach ($this->projectInstalledAgents as $agentKey) { - foreach ($agents as $className => $displayName) { - if (strtolower($agentKey) === strtolower(class_basename($className))) { - $detectedClasses[] = $className; - break; - } + $installedEnvNames = array_unique(array_merge( + $this->projectInstalledCodeEnvironments, + $this->systemInstalledCodeEnvironments + )); + + foreach ($installedEnvNames as $envKey) { + $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name()) + ); + if ($matchingEnv) { + $detectedClasses[] = get_class($matchingEnv); } } - $selectedAgentClasses = collect(multiselect( - label: sprintf('Which agents need AI guidelines for %s?', $this->projectName), - options: $agents, - default: $detectedClasses, - scroll: 4, + $selectedClasses = collect(multiselect( + label: $label, + options: $options->toArray(), + default: array_unique($detectedClasses), + scroll: $type === 'ide' ? 5 : 4, + required: $type === 'ide', + hint: empty($detectedClasses) ? null : sprintf('Auto-detected %s for you', + Arr::join(array_map(fn ($className) => $availableEnvironments->first(fn ($env) => get_class($env) === $className)->displayName(), $detectedClasses), ', ', ' & ') + ) ))->sort(); - return $selectedAgentClasses->map(fn ($agentClass) => new $agentClass); + return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env) => get_class($env) === $className)); } protected function enactGuidelines(): void @@ -424,9 +368,9 @@ protected function enactGuidelines(): void $failed = []; $composedAiGuidelines = $composer->compose(); - $longestAgentName = max(1, ...$this->selectedTargetAgents->map(fn ($agent) => Str::length(class_basename($agent)))->toArray()); + $longestAgentName = max(1, ...$this->selectedTargetAgents->map(fn ($agent) => Str::length($agent->agentName()))->toArray()); foreach ($this->selectedTargetAgents as $agent) { - $agentName = class_basename($agent); + $agentName = $agent->agentName(); $displayAgentName = str_pad($agentName, $longestAgentName); $this->output->write(" {$displayAgentName}... "); @@ -483,10 +427,10 @@ private function enactMcpServers(): void usleep(750000); $failed = []; - $longestIdeName = max(1, ...$this->selectedTargetIdes->map(fn ($ide) => Str::length(class_basename($ide)))->toArray()); + $longestIdeName = max(1, ...$this->selectedTargetIdes->map(fn ($ide) => Str::length($ide->ideName()))->toArray()); foreach ($this->selectedTargetIdes as $ide) { - $ideName = class_basename($ide); + $ideName = $ide->ideName(); $ideDisplay = str_pad($ideName, $longestIdeName); $this->output->write(" {$ideDisplay}... "); $results = []; diff --git a/src/Install/Agents/ClaudeCode.php b/src/Install/Agents/ClaudeCode.php deleted file mode 100644 index 00e480f..0000000 --- a/src/Install/Agents/ClaudeCode.php +++ /dev/null @@ -1,22 +0,0 @@ - $args - * @param array $env - */ - public function installMcp(string $key, string $command, array $args = [], array $env = []): bool - { - $path = $this->mcpPath(); - - // Ensure directory exists - $directory = dirname($path); - if (! is_dir($directory)) { - if (! mkdir($directory, 0755, true)) { - return false; - } - } - - // Read existing configuration or create new one - $config = []; - if (file_exists($path)) { - $content = file_get_contents($path); - if ($content !== false) { - $decoded = json_decode($content, true); - if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { - $config = $decoded; - } - } - } - - // Ensure mcpServers key exists - if (! isset($config[$this->jsonMcpKey])) { - $config[$this->jsonMcpKey] = []; - } - - // Add or update laravel-boost server configuration - $config[$this->jsonMcpKey][$key] = array_filter([ - 'command' => $command, - 'args' => $args, - 'env' => $env, - ]); - - // Write configuration back to file - $json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($json === false) { - return false; - } - - $result = file_put_contents($path, $json); - - return $result !== false; - } -} diff --git a/src/Install/Agents/Junie.php b/src/Install/Agents/Junie.php deleted file mode 100644 index 4d8d99e..0000000 --- a/src/Install/Agents/Junie.php +++ /dev/null @@ -1,20 +0,0 @@ - $args - * @param array $env - */ - public function installMcp(string $key, string $command, array $args = [], array $env = []): bool - { - // -e, --env Set environment variables (e.g. -e KEY=value) - $envString = ''; - foreach ($env as $envKey => $value) { - $envKey = strtoupper($envKey); - $envString .= "-e {$envKey}=\"{$value}\" "; - } - - $command = str_replace([ - '{key}', - '{command}', - '{args}', - '{env}', - ], [ - $key, - $command, - implode(' ', array_map(fn ($arg) => '"'.$arg.'"', $args)), - trim($envString), - ], $this->shellCommand); - - $result = Process::run($command); - - return $result->successful() || $result->seeInErrorOutput('already exists'); - } -} diff --git a/src/Install/Agents/VsCode.php b/src/Install/Agents/VsCode.php deleted file mode 100644 index b580568..0000000 --- a/src/Install/Agents/VsCode.php +++ /dev/null @@ -1,15 +0,0 @@ - ['CLAUDE.md'], ]; } + + public function mcpInstallationStrategy(): McpInstallationStrategy + { + return McpInstallationStrategy::Shell; + } + + public function shellMcpCommand(): string + { + return 'claude mcp add -s local -t stdio {key} "{command}" {args} {env}'; + } + + public function guidelinesPath(): string + { + return 'CLAUDE.md'; + } + + public function frontmatter(): bool + { + return false; + } } diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 95c6299..dec3899 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -4,7 +4,13 @@ namespace Laravel\Boost\Install\CodeEnvironment; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Process; +use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\Ide; use Laravel\Boost\Install\Detection\DetectionStrategyFactory; +use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; abstract class CodeEnvironment @@ -31,7 +37,7 @@ abstract public function displayName(): string; /** * Get the detection configuration for system-wide installation detection. * - * @param \Laravel\Boost\Install\Enums\Platform $platform + * @param Platform $platform * @return array */ abstract public function systemDetectionConfig(Platform $platform): array; @@ -46,7 +52,7 @@ abstract public function projectDetectionConfig(): array; /** * Determine if this code environment is installed on the system. * - * @param \Laravel\Boost\Install\Enums\Platform $platform + * @param Platform $platform * @return bool */ public function detectOnSystem(Platform $platform): bool @@ -70,4 +76,181 @@ public function detectInProject(string $basePath): bool return $strategy->detect($config); } + + /** + * Override this method if the agent name is different from the environment name. + * + * @return ?string + */ + public function agentName(): ?string + { + return $this->name(); + } + + /** + * Override this method if the IDE name is different from the environment name. + * + * @return ?string + */ + public function ideName(): ?string + { + return $this->name(); + } + + /** + * Determine if this environment supports Agent/Guidelines functionality. + * + * @return bool + */ + public function supportsAgent(): bool + { + return $this->agentName() !== null && $this instanceof Agent; + } + + /** + * Determine if this environment supports IDE/MCP functionality. + * + * @return bool + */ + public function supportsIde(): bool + { + return $this->ideName() !== null && $this instanceof Ide; + } + + /** + * Get the MCP installation strategy for this environment. + * + * @return McpInstallationStrategy + */ + public function mcpInstallationStrategy(): McpInstallationStrategy + { + return McpInstallationStrategy::None; + } + + /** + * Get the shell command for MCP installation (shell strategy). + * + * @return ?string + */ + public function shellMcpCommand(): ?string + { + return null; + } + + /** + * Get the path to an MCP configuration file (file strategy). + * + * @return ?string + */ + public function mcpConfigPath(): ?string + { + return null; + } + + /** + * Get the JSON key for MCP servers in the config file (file strategy). + * + * @return string + */ + public function mcpConfigKey(): string + { + return 'mcpServers'; + } + + /** + * Install MCP server using the appropriate strategy. + * + * @param string $key + * @param string $command + * @param array $args + * @param array $env + * @return bool + * @throws FileNotFoundException + */ + public function installMcp(string $key, string $command, array $args = [], array $env = []): bool + { + return match($this->mcpInstallationStrategy()) { + McpInstallationStrategy::Shell => $this->installShellMcp($key, $command, $args, $env), + McpInstallationStrategy::File => $this->installFileMcp($key, $command, $args, $env), + McpInstallationStrategy::None => false, + }; + } + + /** + * Install MCP server using a shell command strategy. + * + * @param string $key + * @param string $command + * @param array $args + * @param array $env + * @return bool + */ + protected function installShellMcp(string $key, string $command, array $args = [], array $env = []): bool + { + $shellCommand = $this->shellMcpCommand(); + if ($shellCommand === null) { + return false; + } + + // Build environment string + $envString = ''; + foreach ($env as $envKey => $value) { + $envKey = strtoupper($envKey); + $envString .= "-e {$envKey}=\"{$value}\" "; + } + + // Replace placeholders in shell command + $command = str_replace([ + '{key}', + '{command}', + '{args}', + '{env}', + ], [ + $key, + $command, + implode(' ', array_map(fn ($arg) => '"'.$arg.'"', $args)), + trim($envString), + ], $shellCommand); + + $result = Process::run($command); + + return $result->successful() || str_contains($result->errorOutput(), 'already exists'); + } + + /** + * Install MCP server using a file-based configuration strategy. + * + * @param string $key + * @param string $command + * @param array $args + * @param array $env + * @return bool + * @throws FileNotFoundException + */ + protected function installFileMcp(string $key, string $command, array $args = [], array $env = []): bool + { + $path = $this->mcpConfigPath(); + if (! $path) { + return false; + } + + // Ensure directory exists + File::ensureDirectoryExists(dirname($path)); + + // Load existing configuration + $config = File::exists($path) + ? json_decode(File::get($path), true) ?: [] + : []; + + $mcpKey = $this->mcpConfigKey(); + data_set($config, "$mcpKey.$key", collect([ + 'command' => $command, + 'args' => $args, + 'env' => $env, + ])->filter()->toArray()); + + $json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + return $json && File::put($path, $json); + } } diff --git a/src/Install/CodeEnvironment/Copilot.php b/src/Install/CodeEnvironment/Copilot.php index a5a03f5..d38d9a7 100644 --- a/src/Install/CodeEnvironment/Copilot.php +++ b/src/Install/CodeEnvironment/Copilot.php @@ -4,9 +4,10 @@ namespace Laravel\Boost\Install\CodeEnvironment; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Install\Enums\Platform; -class Copilot extends CodeEnvironment +class Copilot extends CodeEnvironment implements Agent { public function name(): string { @@ -38,4 +39,18 @@ public function detectOnSystem(Platform $platform): bool return false; } + public function ideName(): ?string + { + return null; + } + + public function guidelinesPath(): string + { + return '.github/copilot-instructions.md'; + } + + public function frontmatter(): bool + { + return false; + } } diff --git a/src/Install/CodeEnvironment/Cursor.php b/src/Install/CodeEnvironment/Cursor.php index 17d36d7..aecb306 100644 --- a/src/Install/CodeEnvironment/Cursor.php +++ b/src/Install/CodeEnvironment/Cursor.php @@ -4,9 +4,12 @@ namespace Laravel\Boost\Install\CodeEnvironment; +use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class Cursor extends CodeEnvironment +class Cursor extends CodeEnvironment implements Agent, Ide { public function name(): string { @@ -47,4 +50,23 @@ public function projectDetectionConfig(): array ]; } + public function mcpInstallationStrategy(): McpInstallationStrategy + { + return McpInstallationStrategy::File; + } + + public function mcpConfigPath(): string + { + return '.cursor/mcp.json'; + } + + public function guidelinesPath(): string + { + return '.cursor/rules/laravel-boost.mdc'; + } + + public function frontmatter(): bool + { + return true; + } } diff --git a/src/Install/CodeEnvironment/PhpStorm.php b/src/Install/CodeEnvironment/PhpStorm.php index 4bc7d8e..d9090b0 100644 --- a/src/Install/CodeEnvironment/PhpStorm.php +++ b/src/Install/CodeEnvironment/PhpStorm.php @@ -4,9 +4,12 @@ namespace Laravel\Boost\Install\CodeEnvironment; +use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class PhpStorm extends CodeEnvironment +class PhpStorm extends CodeEnvironment implements Agent, Ide { public function name(): string { @@ -48,4 +51,28 @@ public function projectDetectionConfig(): array ]; } + public function agentName(): string + { + return 'junie'; + } + + public function mcpInstallationStrategy(): McpInstallationStrategy + { + return McpInstallationStrategy::File; + } + + public function mcpConfigPath(): string + { + return '.junie/mcp/mcp.json'; + } + + public function guidelinesPath(): string + { + return '.junie/guidelines.md'; + } + + public function frontmatter(): bool + { + return false; + } } diff --git a/src/Install/CodeEnvironment/VSCode.php b/src/Install/CodeEnvironment/VSCode.php index 49fd9e6..0165e42 100644 --- a/src/Install/CodeEnvironment/VSCode.php +++ b/src/Install/CodeEnvironment/VSCode.php @@ -4,9 +4,11 @@ namespace Laravel\Boost\Install\CodeEnvironment; +use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class VSCode extends CodeEnvironment +class VSCode extends CodeEnvironment implements Ide { public function name(): string { @@ -43,4 +45,23 @@ public function projectDetectionConfig(): array ]; } + public function agentName(): ?string + { + return null; + } + + public function mcpInstallationStrategy(): McpInstallationStrategy + { + return McpInstallationStrategy::File; + } + + public function mcpConfigPath(): string + { + return '.vscode/mcp.json'; + } + + public function mcpConfigKey(): string + { + return 'servers'; + } } diff --git a/src/Install/CodeEnvironment/Zed.php b/src/Install/CodeEnvironment/Zed.php index 3717b22..98f732c 100644 --- a/src/Install/CodeEnvironment/Zed.php +++ b/src/Install/CodeEnvironment/Zed.php @@ -4,9 +4,11 @@ namespace Laravel\Boost\Install\CodeEnvironment; +use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class Zed extends CodeEnvironment +class Zed extends CodeEnvironment implements Ide { public function name(): string { @@ -47,4 +49,18 @@ public function projectDetectionConfig(): array ]; } + public function agentName(): ?string + { + return null; + } + + public function mcpInstallationStrategy(): McpInstallationStrategy + { + return McpInstallationStrategy::File; + } + + public function mcpConfigPath(): string + { + return '.zed/mcp.json'; + } } diff --git a/src/Install/CodeEnvironmentsDetector.php b/src/Install/CodeEnvironmentsDetector.php index 4f243a1..f7581f1 100644 --- a/src/Install/CodeEnvironmentsDetector.php +++ b/src/Install/CodeEnvironmentsDetector.php @@ -73,4 +73,14 @@ private function getAllPrograms(): Collection fn (string $className) => $this->container->make($className) ); } + + /** + * Get all registered programs (public access). + * + * @return Collection + */ + public function getCodeEnvironments(): Collection + { + return $this->getAllPrograms(); + } } diff --git a/src/Install/Enums/McpInstallationStrategy.php b/src/Install/Enums/McpInstallationStrategy.php new file mode 100644 index 0000000..85a52f9 --- /dev/null +++ b/src/Install/Enums/McpInstallationStrategy.php @@ -0,0 +1,12 @@ + Date: Tue, 12 Aug 2025 14:00:49 +0000 Subject: [PATCH 02/16] Fix code styling --- src/Install/CodeEnvironment/CodeEnvironment.php | 4 +++- src/Install/Enums/McpInstallationStrategy.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index dec3899..a260ed3 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -165,6 +165,7 @@ public function mcpConfigKey(): string * @param array $args * @param array $env * @return bool + * * @throws FileNotFoundException */ public function installMcp(string $key, string $command, array $args = [], array $env = []): bool @@ -225,6 +226,7 @@ protected function installShellMcp(string $key, string $command, array $args = [ * @param array $args * @param array $env * @return bool + * * @throws FileNotFoundException */ protected function installFileMcp(string $key, string $command, array $args = [], array $env = []): bool @@ -243,7 +245,7 @@ protected function installFileMcp(string $key, string $command, array $args = [] : []; $mcpKey = $this->mcpConfigKey(); - data_set($config, "$mcpKey.$key", collect([ + data_set($config, "{$mcpKey}.{$key}", collect([ 'command' => $command, 'args' => $args, 'env' => $env, diff --git a/src/Install/Enums/McpInstallationStrategy.php b/src/Install/Enums/McpInstallationStrategy.php index 85a52f9..a4ec3d1 100644 --- a/src/Install/Enums/McpInstallationStrategy.php +++ b/src/Install/Enums/McpInstallationStrategy.php @@ -9,4 +9,4 @@ enum McpInstallationStrategy: string case Shell = 'shell'; case File = 'file'; case None = 'none'; -} \ No newline at end of file +} From ca6a45ecf69b277d9d9cf315bf7331ec27ce9878 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 19:58:30 +0530 Subject: [PATCH 03/16] refactor: replace Agent and Ide contracts with CodingAgent and McpClient - Removed `Agent` and `Ide` contracts, introducing `CodingAgent` and `McpClient` interfaces. - Updated `CodeEnvironment` subclasses to implement the new contracts. - Renamed and adjusted methods in `CodeEnvironment` for better clarity (`supportsAgent` -> `isCodingAgent`, `supportsIde` -> `isMcpClient`). - Updated `GuidelineWriter` and related tests to use `CodingAgent`. - Simplified `InstallCommand` filtering logic by adopting new contract methods. --- src/Console/InstallCommand.php | 7 +--- src/Contracts/Agent.php | 13 ------- src/Contracts/CodingAgent.php | 25 ++++++++++++ src/Contracts/Ide.php | 17 --------- src/Contracts/McpClient.php | 22 +++++++++++ src/Install/CodeEnvironment/ClaudeCode.php | 6 +-- .../CodeEnvironment/CodeEnvironment.php | 14 +++---- src/Install/CodeEnvironment/Copilot.php | 4 +- src/Install/CodeEnvironment/Cursor.php | 6 +-- src/Install/CodeEnvironment/PhpStorm.php | 6 +-- src/Install/CodeEnvironment/VSCode.php | 4 +- src/Install/CodeEnvironment/Zed.php | 4 +- src/Install/GuidelineWriter.php | 4 +- tests/Unit/Install/GuidelineWriterTest.php | 38 +++++++++---------- 14 files changed, 91 insertions(+), 79 deletions(-) delete mode 100644 src/Contracts/Agent.php create mode 100644 src/Contracts/CodingAgent.php delete mode 100644 src/Contracts/Ide.php create mode 100644 src/Contracts/McpClient.php diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 93a589f..2198f15 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -294,22 +294,19 @@ private function selectCodeEnvironments(string $type, string $label): Collection { $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); - // Filter by type capability $availableEnvironments = $allEnvironments->filter(function (CodeEnvironment $environment) use ($type) { - return ($type === 'ide' && $environment->supportsIde()) || - ($type === 'agent' && $environment->supportsAgent()); + return ($type === 'ide' && $environment->isMcpClient()) || + ($type === 'agent' && $environment->IsCodingAgent()); }); if ($availableEnvironments->isEmpty()) { return collect(); } - // Build options for multiselect $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) { return [get_class($environment) => $environment->displayName()]; })->sort(); - // Auto-detect installed environments $detectedClasses = []; $installedEnvNames = array_unique(array_merge( $this->projectInstalledCodeEnvironments, diff --git a/src/Contracts/Agent.php b/src/Contracts/Agent.php deleted file mode 100644 index 59bdc10..0000000 --- a/src/Contracts/Agent.php +++ /dev/null @@ -1,13 +0,0 @@ - $args - * @param array $env - */ - public function installMcp(string $key, string $command, array $args = [], array $env = []): bool; -} diff --git a/src/Contracts/McpClient.php b/src/Contracts/McpClient.php new file mode 100644 index 0000000..ed03d1e --- /dev/null +++ b/src/Contracts/McpClient.php @@ -0,0 +1,22 @@ + $args Command line arguments + * @param array $env Environment variables + * @return bool True if installation succeeded, false otherwise + */ + public function installMcp(string $key, string $command, array $args = [], array $env = []): bool; +} diff --git a/src/Install/CodeEnvironment/ClaudeCode.php b/src/Install/CodeEnvironment/ClaudeCode.php index d86d2d9..e9de3ed 100644 --- a/src/Install/CodeEnvironment/ClaudeCode.php +++ b/src/Install/CodeEnvironment/ClaudeCode.php @@ -4,12 +4,12 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class ClaudeCode extends CodeEnvironment implements Agent, Ide +class ClaudeCode extends CodeEnvironment implements CodingAgent, McpClient { public function name(): string { diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index a260ed3..45d536b 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -7,8 +7,8 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Detection\DetectionStrategyFactory; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; @@ -102,9 +102,9 @@ public function ideName(): ?string * * @return bool */ - public function supportsAgent(): bool + public function IsCodingAgent(): bool { - return $this->agentName() !== null && $this instanceof Agent; + return $this->agentName() !== null && $this instanceof CodingAgent; } /** @@ -112,9 +112,9 @@ public function supportsAgent(): bool * * @return bool */ - public function supportsIde(): bool + public function isMcpClient(): bool { - return $this->ideName() !== null && $this instanceof Ide; + return $this->ideName() !== null && $this instanceof McpClient; } /** @@ -236,10 +236,8 @@ protected function installFileMcp(string $key, string $command, array $args = [] return false; } - // Ensure directory exists File::ensureDirectoryExists(dirname($path)); - // Load existing configuration $config = File::exists($path) ? json_decode(File::get($path), true) ?: [] : []; diff --git a/src/Install/CodeEnvironment/Copilot.php b/src/Install/CodeEnvironment/Copilot.php index d38d9a7..b155aa1 100644 --- a/src/Install/CodeEnvironment/Copilot.php +++ b/src/Install/CodeEnvironment/Copilot.php @@ -4,10 +4,10 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\CodingAgent; use Laravel\Boost\Install\Enums\Platform; -class Copilot extends CodeEnvironment implements Agent +class Copilot extends CodeEnvironment implements CodingAgent { public function name(): string { diff --git a/src/Install/CodeEnvironment/Cursor.php b/src/Install/CodeEnvironment/Cursor.php index aecb306..75833a8 100644 --- a/src/Install/CodeEnvironment/Cursor.php +++ b/src/Install/CodeEnvironment/Cursor.php @@ -4,12 +4,12 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class Cursor extends CodeEnvironment implements Agent, Ide +class Cursor extends CodeEnvironment implements CodingAgent, McpClient { public function name(): string { diff --git a/src/Install/CodeEnvironment/PhpStorm.php b/src/Install/CodeEnvironment/PhpStorm.php index d9090b0..cd870e0 100644 --- a/src/Install/CodeEnvironment/PhpStorm.php +++ b/src/Install/CodeEnvironment/PhpStorm.php @@ -4,12 +4,12 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\Agent; -use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class PhpStorm extends CodeEnvironment implements Agent, Ide +class PhpStorm extends CodeEnvironment implements CodingAgent, McpClient { public function name(): string { diff --git a/src/Install/CodeEnvironment/VSCode.php b/src/Install/CodeEnvironment/VSCode.php index 0165e42..d3e84fb 100644 --- a/src/Install/CodeEnvironment/VSCode.php +++ b/src/Install/CodeEnvironment/VSCode.php @@ -4,11 +4,11 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class VSCode extends CodeEnvironment implements Ide +class VSCode extends CodeEnvironment implements McpClient { public function name(): string { diff --git a/src/Install/CodeEnvironment/Zed.php b/src/Install/CodeEnvironment/Zed.php index 98f732c..26636d2 100644 --- a/src/Install/CodeEnvironment/Zed.php +++ b/src/Install/CodeEnvironment/Zed.php @@ -4,11 +4,11 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\Ide; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class Zed extends CodeEnvironment implements Ide +class Zed extends CodeEnvironment implements McpClient { public function name(): string { diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index 0821808..5231196 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -4,7 +4,7 @@ namespace Laravel\Boost\Install; -use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\CodingAgent; class GuidelineWriter { @@ -16,7 +16,7 @@ class GuidelineWriter public const NOOP = 3; - public function __construct(protected Agent $agent) + public function __construct(protected CodingAgent $agent) { } diff --git a/tests/Unit/Install/GuidelineWriterTest.php b/tests/Unit/Install/GuidelineWriterTest.php index 2571994..42b8bde 100644 --- a/tests/Unit/Install/GuidelineWriterTest.php +++ b/tests/Unit/Install/GuidelineWriterTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\CodingAgent; use Laravel\Boost\Install\GuidelineWriter; test('it returns NOOP when guidelines are empty', function () { - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn('/tmp/test.md'); $writer = new GuidelineWriter($agent); @@ -19,7 +19,7 @@ $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); $filePath = $tempDir.'/subdir/test.md'; - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($filePath); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -39,7 +39,7 @@ // Use a path that cannot be created (root directory with insufficient permissions) $filePath = '/root/boost_test/test.md'; - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($filePath); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -52,7 +52,7 @@ test('it writes guidelines to new file', function () { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -69,7 +69,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -87,7 +87,7 @@ $initialContent = "# Header\n\n\nold guidelines\n\n\n# Footer"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -105,7 +105,7 @@ $initialContent = "Start\n\nline 1\nline 2\nline 3\n\nEnd"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -124,7 +124,7 @@ $initialContent = "Start\n\nfirst\n\nMiddle\n\nsecond\n\nEnd"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -142,7 +142,7 @@ // Use a directory path instead of file path to cause fopen to fail $dirPath = sys_get_temp_dir(); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($dirPath); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -157,7 +157,7 @@ $initialContent = "# Title\n\nParagraph 1\n\nParagraph 2"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -174,7 +174,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, ''); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -191,7 +191,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, " \n\n \t \n"); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -209,7 +209,7 @@ $initialContent = "# Title\n\n\nShould not be touched\n\n\n\nOld guidelines\n\n\n\nAlso untouched\n"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -228,7 +228,7 @@ $initialContent = "# My Project\n\n\nold guidelines\n\n\n# User Added Section\nThis content was added by the user after the guidelines.\n\n## Another user section\nMore content here."; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -267,7 +267,7 @@ // Give the locking process time to acquire the lock usleep(100000); // 100ms - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -288,7 +288,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(true); @@ -305,7 +305,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "---\ncustomOption: true\n---\n# Existing content\n\nSome text here."); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(true); @@ -322,7 +322,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); - $agent = Mockery::mock(Agent::class); + $agent = Mockery::mock(CodingAgent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); From 447a5dca6dc4d44863eceb76a0801cf6e196adea Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 20:24:31 +0530 Subject: [PATCH 04/16] refactor: integrate CodingAgent contract in InstallCommand and enhance agent name handling logic --- src/Console/InstallCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 2198f15..4366c61 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -9,6 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Laravel\Boost\Contracts\CodingAgent; use Laravel\Boost\Install\Cli\DisplayHelper; use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment; use Laravel\Boost\Install\CodeEnvironmentsDetector; @@ -366,6 +367,7 @@ protected function enactGuidelines(): void $composedAiGuidelines = $composer->compose(); $longestAgentName = max(1, ...$this->selectedTargetAgents->map(fn ($agent) => Str::length($agent->agentName()))->toArray()); + /** @var CodingAgent $agent */ foreach ($this->selectedTargetAgents as $agent) { $agentName = $agent->agentName(); $displayAgentName = str_pad($agentName, $longestAgentName); From 7e51cf87c203f2d8947960852bf11f5c69f51a38 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 20:25:36 +0530 Subject: [PATCH 05/16] refactor: adjust InstallCommand docblocks and structure for consistency and clarity --- src/Console/InstallCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 4366c61..5dbe118 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -367,12 +367,12 @@ protected function enactGuidelines(): void $composedAiGuidelines = $composer->compose(); $longestAgentName = max(1, ...$this->selectedTargetAgents->map(fn ($agent) => Str::length($agent->agentName()))->toArray()); - /** @var CodingAgent $agent */ + /** @var CodeEnvironment $agent */ foreach ($this->selectedTargetAgents as $agent) { $agentName = $agent->agentName(); $displayAgentName = str_pad($agentName, $longestAgentName); $this->output->write(" {$displayAgentName}... "); - + /** @var CodingAgent $agent */ try { (new GuidelineWriter($agent)) ->write($composedAiGuidelines); From 1a809640fa8fe1e0631293e8b03292c930dea196 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 21:53:12 +0530 Subject: [PATCH 06/16] refactor: rename CodingAgent to Agent, streamline detection methods, and remove unused MCP strategies - Replaced `CodingAgent` interface with `Agent` and updated all references. - Removed redundant `mcpInstallationStrategy` and `frontmatter` methods in favor of simplified logic. - Improved `CodeEnvironment` with consolidated detection methods (`isCodingAgent` -> `IsAgent`). - Adjusted related classes, tests, and docblocks for consistency and clarity. --- src/Console/InstallCommand.php | 6 +- src/Contracts/{CodingAgent.php => Agent.php} | 2 +- src/Install/CodeEnvironment/ClaudeCode.php | 9 +- .../CodeEnvironment/CodeEnvironment.php | 108 ++++-------------- src/Install/CodeEnvironment/Copilot.php | 9 +- src/Install/CodeEnvironment/Cursor.php | 10 +- src/Install/CodeEnvironment/PhpStorm.php | 15 +-- src/Install/CodeEnvironment/VSCode.php | 6 - src/Install/CodeEnvironment/Zed.php | 6 - src/Install/GuidelineWriter.php | 15 +-- tests/Unit/Install/GuidelineWriterTest.php | 38 +++--- 11 files changed, 64 insertions(+), 160 deletions(-) rename src/Contracts/{CodingAgent.php => Agent.php} (95%) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 5dbe118..fc25b2b 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -9,7 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Install\Cli\DisplayHelper; use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment; use Laravel\Boost\Install\CodeEnvironmentsDetector; @@ -297,7 +297,7 @@ private function selectCodeEnvironments(string $type, string $label): Collection $availableEnvironments = $allEnvironments->filter(function (CodeEnvironment $environment) use ($type) { return ($type === 'ide' && $environment->isMcpClient()) || - ($type === 'agent' && $environment->IsCodingAgent()); + ($type === 'agent' && $environment->IsAgent()); }); if ($availableEnvironments->isEmpty()) { @@ -372,7 +372,7 @@ protected function enactGuidelines(): void $agentName = $agent->agentName(); $displayAgentName = str_pad($agentName, $longestAgentName); $this->output->write(" {$displayAgentName}... "); - /** @var CodingAgent $agent */ + /** @var Agent $agent */ try { (new GuidelineWriter($agent)) ->write($composedAiGuidelines); diff --git a/src/Contracts/CodingAgent.php b/src/Contracts/Agent.php similarity index 95% rename from src/Contracts/CodingAgent.php rename to src/Contracts/Agent.php index bbfbf78..e703c90 100644 --- a/src/Contracts/CodingAgent.php +++ b/src/Contracts/Agent.php @@ -7,7 +7,7 @@ /** * Agent contract for AI coding assistants that receive guidelines. */ -interface CodingAgent +interface Agent { /** * Get the file path where AI guidelines should be written. diff --git a/src/Install/CodeEnvironment/ClaudeCode.php b/src/Install/CodeEnvironment/ClaudeCode.php index e9de3ed..0d2ceba 100644 --- a/src/Install/CodeEnvironment/ClaudeCode.php +++ b/src/Install/CodeEnvironment/ClaudeCode.php @@ -4,12 +4,12 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class ClaudeCode extends CodeEnvironment implements CodingAgent, McpClient +class ClaudeCode extends CodeEnvironment implements McpClient, Agent { public function name(): string { @@ -55,9 +55,4 @@ public function guidelinesPath(): string { return 'CLAUDE.md'; } - - public function frontmatter(): bool - { - return false; - } } diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 45d536b..1d0b203 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -7,7 +7,7 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Detection\DetectionStrategyFactory; use Laravel\Boost\Install\Enums\McpInstallationStrategy; @@ -15,46 +15,39 @@ abstract class CodeEnvironment { - public function __construct( - protected readonly DetectionStrategyFactory $strategyFactory - ) { + public function __construct(protected readonly DetectionStrategyFactory $strategyFactory) + { } - /** - * Get the internal identifier name for this code environment. - * - * @return string - */ abstract public function name(): string; - /** - * Get the human-readable display name for this code environment. - * - * @return string - */ abstract public function displayName(): string; + public function agentName(): ?string + { + return $this->name(); + } + + public function ideName(): ?string + { + return $this->name(); + } + /** * Get the detection configuration for system-wide installation detection. * * @param Platform $platform - * @return array + * @return array{paths?: string[], command?: string, files?: string[]} */ abstract public function systemDetectionConfig(Platform $platform): array; /** * Get the detection configuration for project-specific detection. * - * @return array + * @return array{paths?: string[], files?: string[]} */ abstract public function projectDetectionConfig(): array; - /** - * Determine if this code environment is installed on the system. - * - * @param Platform $platform - * @return bool - */ public function detectOnSystem(Platform $platform): bool { $config = $this->systemDetectionConfig($platform); @@ -63,12 +56,6 @@ public function detectOnSystem(Platform $platform): bool return $strategy->detect($config, $platform); } - /** - * Determine if this code environment is being used in a specific project. - * - * @param string $basePath - * @return bool - */ public function detectInProject(string $basePath): bool { $config = array_merge($this->projectDetectionConfig(), ['basePath' => $basePath]); @@ -77,81 +64,36 @@ public function detectInProject(string $basePath): bool return $strategy->detect($config); } - /** - * Override this method if the agent name is different from the environment name. - * - * @return ?string - */ - public function agentName(): ?string + public function IsAgent(): bool { - return $this->name(); + return $this->agentName() && $this instanceof Agent; } - /** - * Override this method if the IDE name is different from the environment name. - * - * @return ?string - */ - public function ideName(): ?string - { - return $this->name(); - } - - /** - * Determine if this environment supports Agent/Guidelines functionality. - * - * @return bool - */ - public function IsCodingAgent(): bool - { - return $this->agentName() !== null && $this instanceof CodingAgent; - } - - /** - * Determine if this environment supports IDE/MCP functionality. - * - * @return bool - */ public function isMcpClient(): bool { - return $this->ideName() !== null && $this instanceof McpClient; + return $this->ideName() && $this instanceof McpClient; } - /** - * Get the MCP installation strategy for this environment. - * - * @return McpInstallationStrategy - */ public function mcpInstallationStrategy(): McpInstallationStrategy { - return McpInstallationStrategy::None; + return McpInstallationStrategy::File; } - /** - * Get the shell command for MCP installation (shell strategy). - * - * @return ?string - */ public function shellMcpCommand(): ?string { return null; } - /** - * Get the path to an MCP configuration file (file strategy). - * - * @return ?string - */ public function mcpConfigPath(): ?string { return null; } - /** - * Get the JSON key for MCP servers in the config file (file strategy). - * - * @return string - */ + public function frontmatter(): bool + { + return false; + } + public function mcpConfigKey(): string { return 'mcpServers'; @@ -173,7 +115,7 @@ public function installMcp(string $key, string $command, array $args = [], array return match($this->mcpInstallationStrategy()) { McpInstallationStrategy::Shell => $this->installShellMcp($key, $command, $args, $env), McpInstallationStrategy::File => $this->installFileMcp($key, $command, $args, $env), - McpInstallationStrategy::None => false, + McpInstallationStrategy::None => false }; } diff --git a/src/Install/CodeEnvironment/Copilot.php b/src/Install/CodeEnvironment/Copilot.php index b155aa1..1eaf206 100644 --- a/src/Install/CodeEnvironment/Copilot.php +++ b/src/Install/CodeEnvironment/Copilot.php @@ -4,10 +4,10 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Install\Enums\Platform; -class Copilot extends CodeEnvironment implements CodingAgent +class Copilot extends CodeEnvironment implements Agent { public function name(): string { @@ -48,9 +48,4 @@ public function guidelinesPath(): string { return '.github/copilot-instructions.md'; } - - public function frontmatter(): bool - { - return false; - } } diff --git a/src/Install/CodeEnvironment/Cursor.php b/src/Install/CodeEnvironment/Cursor.php index 75833a8..8449a5f 100644 --- a/src/Install/CodeEnvironment/Cursor.php +++ b/src/Install/CodeEnvironment/Cursor.php @@ -4,12 +4,11 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Contracts\McpClient; -use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class Cursor extends CodeEnvironment implements CodingAgent, McpClient +class Cursor extends CodeEnvironment implements Agent, McpClient { public function name(): string { @@ -50,11 +49,6 @@ public function projectDetectionConfig(): array ]; } - public function mcpInstallationStrategy(): McpInstallationStrategy - { - return McpInstallationStrategy::File; - } - public function mcpConfigPath(): string { return '.cursor/mcp.json'; diff --git a/src/Install/CodeEnvironment/PhpStorm.php b/src/Install/CodeEnvironment/PhpStorm.php index cd870e0..34c3221 100644 --- a/src/Install/CodeEnvironment/PhpStorm.php +++ b/src/Install/CodeEnvironment/PhpStorm.php @@ -4,12 +4,11 @@ namespace Laravel\Boost\Install\CodeEnvironment; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Contracts\McpClient; -use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; -class PhpStorm extends CodeEnvironment implements CodingAgent, McpClient +class PhpStorm extends CodeEnvironment implements Agent, McpClient { public function name(): string { @@ -56,11 +55,6 @@ public function agentName(): string return 'junie'; } - public function mcpInstallationStrategy(): McpInstallationStrategy - { - return McpInstallationStrategy::File; - } - public function mcpConfigPath(): string { return '.junie/mcp/mcp.json'; @@ -70,9 +64,4 @@ public function guidelinesPath(): string { return '.junie/guidelines.md'; } - - public function frontmatter(): bool - { - return false; - } } diff --git a/src/Install/CodeEnvironment/VSCode.php b/src/Install/CodeEnvironment/VSCode.php index d3e84fb..5c0b257 100644 --- a/src/Install/CodeEnvironment/VSCode.php +++ b/src/Install/CodeEnvironment/VSCode.php @@ -5,7 +5,6 @@ namespace Laravel\Boost\Install\CodeEnvironment; use Laravel\Boost\Contracts\McpClient; -use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; class VSCode extends CodeEnvironment implements McpClient @@ -50,11 +49,6 @@ public function agentName(): ?string return null; } - public function mcpInstallationStrategy(): McpInstallationStrategy - { - return McpInstallationStrategy::File; - } - public function mcpConfigPath(): string { return '.vscode/mcp.json'; diff --git a/src/Install/CodeEnvironment/Zed.php b/src/Install/CodeEnvironment/Zed.php index 26636d2..975fe0d 100644 --- a/src/Install/CodeEnvironment/Zed.php +++ b/src/Install/CodeEnvironment/Zed.php @@ -5,7 +5,6 @@ namespace Laravel\Boost\Install\CodeEnvironment; use Laravel\Boost\Contracts\McpClient; -use Laravel\Boost\Install\Enums\McpInstallationStrategy; use Laravel\Boost\Install\Enums\Platform; class Zed extends CodeEnvironment implements McpClient @@ -54,11 +53,6 @@ public function agentName(): ?string return null; } - public function mcpInstallationStrategy(): McpInstallationStrategy - { - return McpInstallationStrategy::File; - } - public function mcpConfigPath(): string { return '.zed/mcp.json'; diff --git a/src/Install/GuidelineWriter.php b/src/Install/GuidelineWriter.php index 5231196..13f828f 100644 --- a/src/Install/GuidelineWriter.php +++ b/src/Install/GuidelineWriter.php @@ -4,7 +4,8 @@ namespace Laravel\Boost\Install; -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; +use RuntimeException; class GuidelineWriter { @@ -16,7 +17,7 @@ class GuidelineWriter public const NOOP = 3; - public function __construct(protected CodingAgent $agent) + public function __construct(protected Agent $agent) { } @@ -34,13 +35,13 @@ public function write(string $guidelines): int $directory = dirname($filePath); if (! is_dir($directory)) { if (! mkdir($directory, 0755, true)) { - throw new \RuntimeException("Failed to create directory: {$directory}"); + throw new RuntimeException("Failed to create directory: {$directory}"); } } $handle = fopen($filePath, 'c+'); if (! $handle) { - throw new \RuntimeException("Failed to open file: {$filePath}"); + throw new RuntimeException("Failed to open file: {$filePath}"); } try { @@ -72,11 +73,11 @@ public function write(string $guidelines): int } if (ftruncate($handle, 0) === false || fseek($handle, 0) === -1) { - throw new \RuntimeException("Failed to reset file pointer: {$filePath}"); + throw new RuntimeException("Failed to reset file pointer: {$filePath}"); } if (fwrite($handle, $newContent) === false) { - throw new \RuntimeException("Failed to write to file: {$filePath}"); + throw new RuntimeException("Failed to write to file: {$filePath}"); } flock($handle, LOCK_UN); @@ -100,7 +101,7 @@ private function acquireLockWithRetry(mixed $handle, string $filePath, int $maxR $attempts++; if ($attempts >= $maxRetries) { - throw new \RuntimeException("Failed to acquire lock on file after {$maxRetries} attempts: {$filePath}"); + throw new RuntimeException("Failed to acquire lock on file after {$maxRetries} attempts: {$filePath}"); } // Exponential backoff with jitter diff --git a/tests/Unit/Install/GuidelineWriterTest.php b/tests/Unit/Install/GuidelineWriterTest.php index 42b8bde..2571994 100644 --- a/tests/Unit/Install/GuidelineWriterTest.php +++ b/tests/Unit/Install/GuidelineWriterTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -use Laravel\Boost\Contracts\CodingAgent; +use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Install\GuidelineWriter; test('it returns NOOP when guidelines are empty', function () { - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn('/tmp/test.md'); $writer = new GuidelineWriter($agent); @@ -19,7 +19,7 @@ $tempDir = sys_get_temp_dir().'/boost_test_'.uniqid(); $filePath = $tempDir.'/subdir/test.md'; - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($filePath); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -39,7 +39,7 @@ // Use a path that cannot be created (root directory with insufficient permissions) $filePath = '/root/boost_test/test.md'; - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($filePath); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -52,7 +52,7 @@ test('it writes guidelines to new file', function () { $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -69,7 +69,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -87,7 +87,7 @@ $initialContent = "# Header\n\n\nold guidelines\n\n\n# Footer"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -105,7 +105,7 @@ $initialContent = "Start\n\nline 1\nline 2\nline 3\n\nEnd"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -124,7 +124,7 @@ $initialContent = "Start\n\nfirst\n\nMiddle\n\nsecond\n\nEnd"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -142,7 +142,7 @@ // Use a directory path instead of file path to cause fopen to fail $dirPath = sys_get_temp_dir(); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($dirPath); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -157,7 +157,7 @@ $initialContent = "# Title\n\nParagraph 1\n\nParagraph 2"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -174,7 +174,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, ''); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -191,7 +191,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, " \n\n \t \n"); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -209,7 +209,7 @@ $initialContent = "# Title\n\n\nShould not be touched\n\n\n\nOld guidelines\n\n\n\nAlso untouched\n"; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -228,7 +228,7 @@ $initialContent = "# My Project\n\n\nold guidelines\n\n\n# User Added Section\nThis content was added by the user after the guidelines.\n\n## Another user section\nMore content here."; file_put_contents($tempFile, $initialContent); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -267,7 +267,7 @@ // Give the locking process time to acquire the lock usleep(100000); // 100ms - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); @@ -288,7 +288,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(true); @@ -305,7 +305,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "---\ncustomOption: true\n---\n# Existing content\n\nSome text here."); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(true); @@ -322,7 +322,7 @@ $tempFile = tempnam(sys_get_temp_dir(), 'boost_test_'); file_put_contents($tempFile, "# Existing content\n\nSome text here."); - $agent = Mockery::mock(CodingAgent::class); + $agent = Mockery::mock(Agent::class); $agent->shouldReceive('guidelinesPath')->andReturn($tempFile); $agent->shouldReceive('frontmatter')->andReturn(false); From a8290fb1c5794ac57041141ae576a5138047ca11 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 22:32:36 +0530 Subject: [PATCH 07/16] refactor: add unit tests for CodeEnvironment and update InstallCommand for MCP handling - Introduced comprehensive tests for `CodeEnvironment`, covering detection, MCP installation, and interface logic. - Renamed variables in `InstallCommand` for clarity (`selectedTargetIdes` -> `selectedTargetMcpClient`). - Replaced `enact` method in `InstallCommand` with `performInstallation`. - Adjusted MCP installation logic for streamlined and consistent implementation. --- src/Console/InstallCommand.php | 49 ++- .../CodeEnvironment/CodeEnvironmentTest.php | 365 ++++++++++++++++++ 2 files changed, 395 insertions(+), 19 deletions(-) create mode 100644 tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index fc25b2b..3069127 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -42,7 +42,7 @@ class InstallCommand extends Command private Collection $selectedTargetAgents; /** @var Collection */ - private Collection $selectedTargetIdes; + private Collection $selectedTargetMcpClient; /** @var Collection */ private Collection $selectedBoostFeatures; @@ -67,7 +67,7 @@ public function handle(CodeEnvironmentsDetector $codeEnvironmentsDetector, Herd $this->displayBoostHeader(); $this->discoverEnvironment(); $this->collectInstallationPreferences(); - $this->enact(); + $this->performInstallation(); $this->outro(); } @@ -82,7 +82,7 @@ private function bootstrap(CodeEnvironmentsDetector $codeEnvironmentsDetector, H $this->redCross = $this->red('✗'); $this->selectedTargetAgents = collect(); - $this->selectedTargetIdes = collect(); + $this->selectedTargetMcpClient = collect(); $this->projectName = basename(base_path()); } @@ -117,20 +117,18 @@ private function collectInstallationPreferences(): void { $this->selectedBoostFeatures = $this->selectBoostFeatures(); $this->enforceTests = $this->determineTestEnforcement(ask: false); - $this->selectedTargetIdes = $this->selectTargetIdes(); + $this->selectedTargetMcpClient = $this->selectTargetIdes(); $this->selectedTargetAgents = $this->selectTargetAgents(); } - private function enact(): void + private function performInstallation(): void { - if ($this->shouldInstallAiGuidelines() && $this->selectedTargetAgents->isNotEmpty()) { - $this->enactGuidelines(); - } + $this->installGuidelines(); usleep(750000); - if (($this->shouldInstallMcp() || $this->shouldInstallHerdMcp()) && $this->selectedTargetIdes->isNotEmpty()) { - $this->enactMcpServers(); + if (($this->shouldInstallMcp() || $this->shouldInstallHerdMcp()) && $this->selectedTargetMcpClient->isNotEmpty()) { + $this->installMcpServerConfig(); } } @@ -159,7 +157,7 @@ private function outro(): void { $label = 'https://boost.laravel.com/installed'; - $ideNames = $this->selectedTargetIdes->map(fn ($ide) => 'i:'.$ide->ideName())->toArray(); + $ideNames = $this->selectedTargetMcpClient->map(fn ($ide) => 'i:'.$ide->ideName())->toArray(); $agentNames = $this->selectedTargetAgents->map(fn ($agent) => 'a:'.$agent->agentName())->toArray(); $boostFeatures = $this->selectedBoostFeatures->map(fn ($feature) => 'b:'.$feature)->toArray(); @@ -336,7 +334,7 @@ private function selectCodeEnvironments(string $type, string $label): Collection return $selectedClasses->map(fn ($className) => $availableEnvironments->first(fn ($env) => get_class($env) === $className)); } - protected function enactGuidelines(): void + private function installGuidelines(): void { if (! $this->shouldInstallAiGuidelines()) { return; @@ -417,8 +415,17 @@ private function shouldInstallHerdMcp(): bool return $this->selectedBoostFeatures->contains('herd_mcp'); } - private function enactMcpServers(): void + private function installMcpServerConfig(): void { + if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { + return; + } + + if ($this->selectedTargetMcpClient->isEmpty()) { + $this->info('No agents selected for guideline installation.'); + + return; + } $this->newLine(); $this->info(' Installing MCP servers to your selected IDEs'); $this->newLine(); @@ -426,18 +433,22 @@ private function enactMcpServers(): void usleep(750000); $failed = []; - $longestIdeName = max(1, ...$this->selectedTargetIdes->map(fn ($ide) => Str::length($ide->ideName()))->toArray()); + $longestIdeName = max( + 1, + ...$this->selectedTargetMcpClient->map( + fn ($mcpClient) => Str::length($mcpClient->ideName()) + )->toArray() + ); - foreach ($this->selectedTargetIdes as $ide) { - $ideName = $ide->ideName(); + foreach ($this->selectedTargetMcpClient as $mcpClient) { + $ideName = $mcpClient->ideName(); $ideDisplay = str_pad($ideName, $longestIdeName); $this->output->write(" {$ideDisplay}... "); $results = []; - // Install Laravel Boost MCP if enabled if ($this->shouldInstallMcp()) { try { - $result = $ide->installMcp('laravel-boost', 'php', ['./artisan', 'boost:mcp']); + $result = $mcpClient->installMcp('laravel-boost', 'php', ['./artisan', 'boost:mcp']); if ($result) { $results[] = $this->greenTick.' Boost'; @@ -454,7 +465,7 @@ private function enactMcpServers(): void // Install Herd MCP if enabled if ($this->shouldInstallHerdMcp()) { try { - $result = $ide->installMcp( + $result = $mcpClient->installMcp( key: 'herd', command: 'php', args: [$this->herd->mcpPath()], diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php new file mode 100644 index 0000000..1463f53 --- /dev/null +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -0,0 +1,365 @@ +strategyFactory = Mockery::mock(DetectionStrategyFactory::class); + $this->strategy = Mockery::mock(DetectionStrategy::class); +}); + +afterEach(function () { + Mockery::close(); +}); + +// Create a concrete test implementation for testing abstract methods +class TestCodeEnvironment extends CodeEnvironment +{ + public function name(): string + { + return 'test'; + } + + public function displayName(): string + { + return 'Test Environment'; + } + + public function systemDetectionConfig(Platform $platform): array + { + return ['paths' => ['/test/path']]; + } + + public function projectDetectionConfig(): array + { + return ['files' => ['test.config']]; + } +} + +class TestAgent extends TestCodeEnvironment implements Agent +{ + public function guidelinesPath(): string + { + return 'test-guidelines.md'; + } +} + +class TestMcpClient extends TestCodeEnvironment implements McpClient +{ + public function mcpConfigPath(): string + { + return '.test/mcp.json'; + } +} + +class TestAgentAndMcpClient extends TestCodeEnvironment implements Agent, McpClient +{ + public function guidelinesPath(): string + { + return 'test-guidelines.md'; + } + + public function mcpConfigPath(): string + { + return '.test/mcp.json'; + } +} + +test('detectOnSystem delegates to strategy factory and detection strategy', function () { + $platform = Platform::Darwin; + $config = ['paths' => ['/test/path']]; + + $this->strategyFactory + ->shouldReceive('makeFromConfig') + ->once() + ->with($config) + ->andReturn($this->strategy); + + $this->strategy + ->shouldReceive('detect') + ->once() + ->with($config, $platform) + ->andReturn(true); + + $environment = new TestCodeEnvironment($this->strategyFactory); + $result = $environment->detectOnSystem($platform); + + expect($result)->toBe(true); +}); + +test('detectInProject merges config with basePath and delegates to strategy', function () { + $basePath = '/project/path'; + $projectConfig = ['files' => ['test.config']]; + $mergedConfig = ['files' => ['test.config'], 'basePath' => $basePath]; + + $this->strategyFactory + ->shouldReceive('makeFromConfig') + ->once() + ->with($mergedConfig) + ->andReturn($this->strategy); + + $this->strategy + ->shouldReceive('detect') + ->once() + ->with($mergedConfig) + ->andReturn(false); + + $environment = new TestCodeEnvironment($this->strategyFactory); + $result = $environment->detectInProject($basePath); + + expect($result)->toBe(false); +}); + +test('agentName returns name by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->agentName())->toBe('test'); +}); + +test('ideName returns name by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->ideName())->toBe('test'); +}); + +test('IsAgent returns true when implements Agent interface and has agentName', function () { + $agent = new TestAgent($this->strategyFactory); + + expect($agent->IsAgent())->toBe(true); +}); + +test('IsAgent returns false when does not implement Agent interface', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->IsAgent())->toBe(false); +}); + +test('isMcpClient returns true when implements McpClient interface and has ideName', function () { + $mcpClient = new TestMcpClient($this->strategyFactory); + + expect($mcpClient->isMcpClient())->toBe(true); +}); + +test('isMcpClient returns false when does not implement McpClient interface', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->isMcpClient())->toBe(false); +}); + +test('mcpInstallationStrategy returns File by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->mcpInstallationStrategy())->toBe(McpInstallationStrategy::File); +}); + +test('shellMcpCommand returns null by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->shellMcpCommand())->toBe(null); +}); + +test('mcpConfigPath returns null by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->mcpConfigPath())->toBe(null); +}); + +test('frontmatter returns false by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->frontmatter())->toBe(false); +}); + +test('mcpConfigKey returns mcpServers by default', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + expect($environment->mcpConfigKey())->toBe('mcpServers'); +}); + +test('installMcp uses Shell strategy when configured', function () { + $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); + $environment->shouldAllowMockingProtectedMethods(); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::Shell); + + $environment->shouldReceive('installShellMcp') + ->once() + ->with('test-key', 'test-command', ['arg1'], ['ENV' => 'value']) + ->andReturn(true); + + $result = $environment->installMcp('test-key', 'test-command', ['arg1'], ['ENV' => 'value']); + + expect($result)->toBe(true); +}); + +test('installMcp uses File strategy when configured', function () { + $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); + $environment->shouldAllowMockingProtectedMethods(); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::File); + + $environment->shouldReceive('installFileMcp') + ->once() + ->with('test-key', 'test-command', ['arg1'], ['ENV' => 'value']) + ->andReturn(true); + + $result = $environment->installMcp('test-key', 'test-command', ['arg1'], ['ENV' => 'value']); + + expect($result)->toBe(true); +}); + +test('installMcp returns false for None strategy', function () { + $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::None); + + $result = $environment->installMcp('test-key', 'test-command'); + + expect($result)->toBe(false); +}); + +test('installShellMcp returns false when shellMcpCommand is null', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + $result = $environment->installMcp('test-key', 'test-command'); + + expect($result)->toBe(false); +}); + +test('installShellMcp executes command with placeholders replaced', function () { + $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); + $environment->shouldAllowMockingProtectedMethods(); + + $environment->shouldReceive('shellMcpCommand') + ->andReturn('install {key} {command} {args} {env}'); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::Shell); + + $mockResult = Mockery::mock(); + $mockResult->shouldReceive('successful')->andReturn(true); + $mockResult->shouldReceive('errorOutput')->andReturn(''); + + Process::shouldReceive('run') + ->once() + ->with(Mockery::on(function ($command) { + return str_contains($command, 'install test-key test-command "arg1" "arg2"') && + str_contains($command, '-e ENV1="value1"') && + str_contains($command, '-e ENV2="value2"'); + })) + ->andReturn($mockResult); + + $result = $environment->installMcp('test-key', 'test-command', ['arg1', 'arg2'], ['env1' => 'value1', 'env2' => 'value2']); + + expect($result)->toBe(true); +}); + +test('installShellMcp returns true when process fails but has already exists error', function () { + $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); + $environment->shouldAllowMockingProtectedMethods(); + + $environment->shouldReceive('shellMcpCommand') + ->andReturn('install {key}'); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::Shell); + + $mockResult = Mockery::mock(); + $mockResult->shouldReceive('successful')->andReturn(false); + $mockResult->shouldReceive('errorOutput')->andReturn('Error: already exists'); + + Process::shouldReceive('run') + ->once() + ->andReturn($mockResult); + + $result = $environment->installMcp('test-key', 'test-command'); + + expect($result)->toBe(true); +}); + +test('installFileMcp returns false when mcpConfigPath is null', function () { + $environment = new TestCodeEnvironment($this->strategyFactory); + + $result = $environment->installMcp('test-key', 'test-command'); + + expect($result)->toBe(false); +}); + +test('installFileMcp creates new config file when none exists', function () { + $environment = Mockery::mock(TestMcpClient::class)->makePartial(); + $environment->shouldAllowMockingProtectedMethods(); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::File); + + File::shouldReceive('ensureDirectoryExists') + ->once() + ->with('.test'); + + File::shouldReceive('exists') + ->once() + ->with('.test/mcp.json') + ->andReturn(false); + + File::shouldReceive('put') + ->once() + ->with('.test/mcp.json', Mockery::type('string')) + ->andReturn(true); + + $result = $environment->installMcp('test-key', 'test-command', ['arg1'], ['ENV' => 'value']); + + expect($result)->toBe(true); +}); + +test('installFileMcp updates existing config file', function () { + $environment = Mockery::mock(TestMcpClient::class)->makePartial(); + $environment->shouldAllowMockingProtectedMethods(); + + $environment->shouldReceive('mcpInstallationStrategy') + ->andReturn(McpInstallationStrategy::File); + + $existingConfig = json_encode(['mcpServers' => ['existing' => ['command' => 'existing-cmd']]]); + + File::shouldReceive('ensureDirectoryExists') + ->once() + ->with('.test'); + + File::shouldReceive('exists') + ->once() + ->with('.test/mcp.json') + ->andReturn(true); + + File::shouldReceive('get') + ->once() + ->with('.test/mcp.json') + ->andReturn($existingConfig); + + File::shouldReceive('put') + ->once() + ->with('.test/mcp.json', Mockery::on(function ($json) { + $config = json_decode($json, true); + + return isset($config['mcpServers']['test-key']) && + isset($config['mcpServers']['existing']); + })) + ->andReturn(true); + + $result = $environment->installMcp('test-key', 'test-command', ['arg1'], ['ENV' => 'value']); + + expect($result)->toBe(true); +}); From a61613489abb16cf3cc0fa15b097c5840d58d12b Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 22:56:30 +0530 Subject: [PATCH 08/16] refactor: rename ideName to mcpClientName and enhance MCP client handling - Replaced `ideName` references with `mcpClientName` for clarity. - Updated `InstallCommand` to use specific type hints (`Agent`, `McpClient`) for better consistency and readability. - Added docblocks and exceptions for improved code documentation and error handling. - Adjusted tests to reflect the `McpClient` changes and renamed corresponding test cases. - Minor updates in `VSCode` and contract interfaces to align with the new naming convention. --- src/Console/InstallCommand.php | 15 ++++++++------- src/Contracts/Agent.php | 7 +++++++ src/Contracts/McpClient.php | 7 +++++++ src/Install/CodeEnvironment/CodeEnvironment.php | 4 ++-- src/Install/CodeEnvironment/Copilot.php | 2 +- src/Install/CodeEnvironment/VSCode.php | 7 +------ .../CodeEnvironment/CodeEnvironmentTest.php | 6 +++--- 7 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 3069127..9be823a 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -10,6 +10,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Laravel\Boost\Contracts\Agent; +use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Cli\DisplayHelper; use Laravel\Boost\Install\CodeEnvironment\CodeEnvironment; use Laravel\Boost\Install\CodeEnvironmentsDetector; @@ -38,10 +39,10 @@ class InstallCommand extends Command private Terminal $terminal; - /** @var Collection */ + /** @var Collection */ private Collection $selectedTargetAgents; - /** @var Collection */ + /** @var Collection */ private Collection $selectedTargetMcpClient; /** @var Collection */ @@ -157,11 +158,11 @@ private function outro(): void { $label = 'https://boost.laravel.com/installed'; - $ideNames = $this->selectedTargetMcpClient->map(fn ($ide) => 'i:'.$ide->ideName())->toArray(); - $agentNames = $this->selectedTargetAgents->map(fn ($agent) => 'a:'.$agent->agentName())->toArray(); + $ideNames = $this->selectedTargetMcpClient->map(fn (McpClient $mcpClient) => 'i:'.$mcpClient->mcpClientName()) + ->toArray(); + $agentNames = $this->selectedTargetAgents->map(fn (Agent $agent) => 'a:'.$agent->agentName())->toArray(); $boostFeatures = $this->selectedBoostFeatures->map(fn ($feature) => 'b:'.$feature)->toArray(); - // Guidelines installed (prefix: g) $guidelines = []; if ($this->shouldInstallAiGuidelines()) { $guidelines[] = 'g:ai'; @@ -436,12 +437,12 @@ private function installMcpServerConfig(): void $longestIdeName = max( 1, ...$this->selectedTargetMcpClient->map( - fn ($mcpClient) => Str::length($mcpClient->ideName()) + fn (McpClient $mcpClient) => Str::length($mcpClient->mcpClientName()) )->toArray() ); foreach ($this->selectedTargetMcpClient as $mcpClient) { - $ideName = $mcpClient->ideName(); + $ideName = $mcpClient->mcpClientName(); $ideDisplay = str_pad($ideName, $longestIdeName); $this->output->write(" {$ideDisplay}... "); $results = []; diff --git a/src/Contracts/Agent.php b/src/Contracts/Agent.php index e703c90..75a55b0 100644 --- a/src/Contracts/Agent.php +++ b/src/Contracts/Agent.php @@ -9,6 +9,13 @@ */ interface Agent { + /** + * Get the display name of the Agent. + * + * @return string|null + */ + public function agentName(): ?string; + /** * Get the file path where AI guidelines should be written. * diff --git a/src/Contracts/McpClient.php b/src/Contracts/McpClient.php index ed03d1e..74629d3 100644 --- a/src/Contracts/McpClient.php +++ b/src/Contracts/McpClient.php @@ -9,6 +9,13 @@ */ interface McpClient { + /** + * Get the display name of the MCP (Model Context Protocol) client. + * + * @return string|null + */ + public function mcpClientName(): ?string; + /** * Install an MCP server configuration in this IDE. * diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 1d0b203..6568300 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -28,7 +28,7 @@ public function agentName(): ?string return $this->name(); } - public function ideName(): ?string + public function mcpClientName(): ?string { return $this->name(); } @@ -71,7 +71,7 @@ public function IsAgent(): bool public function isMcpClient(): bool { - return $this->ideName() && $this instanceof McpClient; + return $this->mcpClientName() && $this instanceof McpClient; } public function mcpInstallationStrategy(): McpInstallationStrategy diff --git a/src/Install/CodeEnvironment/Copilot.php b/src/Install/CodeEnvironment/Copilot.php index 1eaf206..2219539 100644 --- a/src/Install/CodeEnvironment/Copilot.php +++ b/src/Install/CodeEnvironment/Copilot.php @@ -39,7 +39,7 @@ public function detectOnSystem(Platform $platform): bool return false; } - public function ideName(): ?string + public function mcpClientName(): ?string { return null; } diff --git a/src/Install/CodeEnvironment/VSCode.php b/src/Install/CodeEnvironment/VSCode.php index 5c0b257..f7ce3d9 100644 --- a/src/Install/CodeEnvironment/VSCode.php +++ b/src/Install/CodeEnvironment/VSCode.php @@ -16,7 +16,7 @@ public function name(): string public function displayName(): string { - return 'Visual Studio Code'; + return 'Vs Code'; } public function systemDetectionConfig(Platform $platform): array @@ -44,11 +44,6 @@ public function projectDetectionConfig(): array ]; } - public function agentName(): ?string - { - return null; - } - public function mcpConfigPath(): string { return '.vscode/mcp.json'; diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index 1463f53..af0bdfd 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -128,10 +128,10 @@ public function mcpConfigPath(): string expect($environment->agentName())->toBe('test'); }); -test('ideName returns name by default', function () { +test('mcpClientName returns name by default', function () { $environment = new TestCodeEnvironment($this->strategyFactory); - expect($environment->ideName())->toBe('test'); + expect($environment->mcpClientName())->toBe('test'); }); test('IsAgent returns true when implements Agent interface and has agentName', function () { @@ -146,7 +146,7 @@ public function mcpConfigPath(): string expect($environment->IsAgent())->toBe(false); }); -test('isMcpClient returns true when implements McpClient interface and has ideName', function () { +test('isMcpClient returns true when implements McpClient interface and has mcpClientName', function () { $mcpClient = new TestMcpClient($this->strategyFactory); expect($mcpClient->isMcpClient())->toBe(true); From 391ad01549f9701ba0410c602eb91f6a6d968518 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 23:00:14 +0530 Subject: [PATCH 09/16] refactor: update McpInstallationStrategy casing to align with consistent enum naming convention - Renamed enum cases in `McpInstallationStrategy` to uppercase for standardization. - Updated references across codebase and adjusted related test cases. --- src/Install/CodeEnvironment/ClaudeCode.php | 2 +- src/Install/CodeEnvironment/CodeEnvironment.php | 8 ++++---- src/Install/Enums/McpInstallationStrategy.php | 6 +++--- .../CodeEnvironment/CodeEnvironmentTest.php | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Install/CodeEnvironment/ClaudeCode.php b/src/Install/CodeEnvironment/ClaudeCode.php index 0d2ceba..07bc6c5 100644 --- a/src/Install/CodeEnvironment/ClaudeCode.php +++ b/src/Install/CodeEnvironment/ClaudeCode.php @@ -43,7 +43,7 @@ public function projectDetectionConfig(): array public function mcpInstallationStrategy(): McpInstallationStrategy { - return McpInstallationStrategy::Shell; + return McpInstallationStrategy::SHELL; } public function shellMcpCommand(): string diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 6568300..084a90f 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -76,7 +76,7 @@ public function isMcpClient(): bool public function mcpInstallationStrategy(): McpInstallationStrategy { - return McpInstallationStrategy::File; + return McpInstallationStrategy::FILE; } public function shellMcpCommand(): ?string @@ -113,9 +113,9 @@ public function mcpConfigKey(): string public function installMcp(string $key, string $command, array $args = [], array $env = []): bool { return match($this->mcpInstallationStrategy()) { - McpInstallationStrategy::Shell => $this->installShellMcp($key, $command, $args, $env), - McpInstallationStrategy::File => $this->installFileMcp($key, $command, $args, $env), - McpInstallationStrategy::None => false + McpInstallationStrategy::SHELL => $this->installShellMcp($key, $command, $args, $env), + McpInstallationStrategy::FILE => $this->installFileMcp($key, $command, $args, $env), + McpInstallationStrategy::NONE => false }; } diff --git a/src/Install/Enums/McpInstallationStrategy.php b/src/Install/Enums/McpInstallationStrategy.php index a4ec3d1..38383e6 100644 --- a/src/Install/Enums/McpInstallationStrategy.php +++ b/src/Install/Enums/McpInstallationStrategy.php @@ -6,7 +6,7 @@ enum McpInstallationStrategy: string { - case Shell = 'shell'; - case File = 'file'; - case None = 'none'; + case SHELL = 'shell'; + case FILE = 'file'; + case NONE = 'none'; } diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index af0bdfd..a9d88e5 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -161,7 +161,7 @@ public function mcpConfigPath(): string test('mcpInstallationStrategy returns File by default', function () { $environment = new TestCodeEnvironment($this->strategyFactory); - expect($environment->mcpInstallationStrategy())->toBe(McpInstallationStrategy::File); + expect($environment->mcpInstallationStrategy())->toBe(McpInstallationStrategy::FILE); }); test('shellMcpCommand returns null by default', function () { @@ -193,7 +193,7 @@ public function mcpConfigPath(): string $environment->shouldAllowMockingProtectedMethods(); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::Shell); + ->andReturn(McpInstallationStrategy::SHELL); $environment->shouldReceive('installShellMcp') ->once() @@ -210,7 +210,7 @@ public function mcpConfigPath(): string $environment->shouldAllowMockingProtectedMethods(); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::File); + ->andReturn(McpInstallationStrategy::FILE); $environment->shouldReceive('installFileMcp') ->once() @@ -226,7 +226,7 @@ public function mcpConfigPath(): string $environment = Mockery::mock(TestCodeEnvironment::class)->makePartial(); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::None); + ->andReturn(McpInstallationStrategy::NONE); $result = $environment->installMcp('test-key', 'test-command'); @@ -249,7 +249,7 @@ public function mcpConfigPath(): string ->andReturn('install {key} {command} {args} {env}'); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::Shell); + ->andReturn(McpInstallationStrategy::SHELL); $mockResult = Mockery::mock(); $mockResult->shouldReceive('successful')->andReturn(true); @@ -277,7 +277,7 @@ public function mcpConfigPath(): string ->andReturn('install {key}'); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::Shell); + ->andReturn(McpInstallationStrategy::SHELL); $mockResult = Mockery::mock(); $mockResult->shouldReceive('successful')->andReturn(false); @@ -305,7 +305,7 @@ public function mcpConfigPath(): string $environment->shouldAllowMockingProtectedMethods(); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::File); + ->andReturn(McpInstallationStrategy::FILE); File::shouldReceive('ensureDirectoryExists') ->once() @@ -331,7 +331,7 @@ public function mcpConfigPath(): string $environment->shouldAllowMockingProtectedMethods(); $environment->shouldReceive('mcpInstallationStrategy') - ->andReturn(McpInstallationStrategy::File); + ->andReturn(McpInstallationStrategy::FILE); $existingConfig = json_encode(['mcpServers' => ['existing' => ['command' => 'existing-cmd']]]); From 0ed0b93fe49f1d91de1199dd5abf6b9c43973966 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 23:33:27 +0530 Subject: [PATCH 10/16] refactor: enhance InstallCommand with improved contract handling and rename methods for consistency - Updated method names in `InstallCommand` to reflect contract-specific responsibilities (`selectTargetIdes` -> `selectTargetMcpClients`). - Introduced `getSelectionConfig` to centralize configuration for contract-based selection. - Standardized `CodeEnvironment` method behaviors (`name()` -> `displayName()`). - Adjusted tests to align with updated method and property names. - Included exception handling for unsupported contract classes. --- src/Console/InstallCommand.php | 59 +++++++++++++------ .../CodeEnvironment/CodeEnvironment.php | 4 +- src/Install/CodeEnvironment/PhpStorm.php | 2 +- .../CodeEnvironment/CodeEnvironmentTest.php | 8 +-- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 9be823a..d65d959 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -9,6 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use InvalidArgumentException; use Laravel\Boost\Contracts\Agent; use Laravel\Boost\Contracts\McpClient; use Laravel\Boost\Install\Cli\DisplayHelper; @@ -118,7 +119,7 @@ private function collectInstallationPreferences(): void { $this->selectedBoostFeatures = $this->selectBoostFeatures(); $this->enforceTests = $this->determineTestEnforcement(ask: false); - $this->selectedTargetMcpClient = $this->selectTargetIdes(); + $this->selectedTargetMcpClient = $this->selectTargetMcpClients(); $this->selectedTargetAgents = $this->selectTargetAgents(); } @@ -143,9 +144,9 @@ private function discoverTools(): array ->name('*.php'); foreach ($finder as $toolFile) { - $fqdn = 'Laravel\\Boost\\Mcp\\Tools\\'.$toolFile->getBasename('.php'); - if (class_exists($fqdn)) { - $tools[$fqdn] = Str::headline($toolFile->getBasename('.php')); + $fullyClassifiedClassName = 'Laravel\\Boost\\Mcp\\Tools\\'.$toolFile->getBasename('.php'); + if (class_exists($fullyClassifiedClassName)) { + $tools[$fullyClassifiedClassName] = Str::headline($toolFile->getBasename('.php')); } } @@ -260,14 +261,14 @@ protected function boostToolsToDisable(): array /** * @return Collection */ - private function selectTargetIdes(): Collection + private function selectTargetMcpClients(): Collection { if (! $this->shouldInstallMcp() && ! $this->shouldInstallHerdMcp()) { return collect(); } return $this->selectCodeEnvironments( - 'ide', + McpClient::class, sprintf('Which code editors do you use in %s?', $this->projectName) ); } @@ -282,29 +283,47 @@ private function selectTargetAgents(): Collection } return $this->selectCodeEnvironments( - 'agent', + Agent::class, sprintf('Which agents need AI guidelines for %s?', $this->projectName) ); } + /** + * Get configuration settings for contract-specific selection behavior. + * + * @param string $contractClass + * @return array{scroll: int, required: bool, displayMethod: string} + */ + private function getSelectionConfig(string $contractClass): array + { + return match($contractClass) { + Agent::class => ['scroll' => 4, 'required' => false, 'displayMethod' => 'agentName'], + McpClient::class => ['scroll' => 5, 'required' => true, 'displayMethod' => 'displayName'], + default => throw new InvalidArgumentException("Unsupported contract class: {$contractClass}"), + }; + } + /** * @return Collection */ - private function selectCodeEnvironments(string $type, string $label): Collection + private function selectCodeEnvironments(string $contractClass, string $label): Collection { $allEnvironments = $this->codeEnvironmentsDetector->getCodeEnvironments(); + $config = $this->getSelectionConfig($contractClass); - $availableEnvironments = $allEnvironments->filter(function (CodeEnvironment $environment) use ($type) { - return ($type === 'ide' && $environment->isMcpClient()) || - ($type === 'agent' && $environment->IsAgent()); + $availableEnvironments = $allEnvironments->filter(function (CodeEnvironment $environment) use ($contractClass) { + return $environment instanceof $contractClass; }); if ($availableEnvironments->isEmpty()) { return collect(); } - $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) { - return [get_class($environment) => $environment->displayName()]; + $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) use ($config) { + $displayMethod = $config['displayMethod']; + $displayText = $environment->{$displayMethod}(); + + return [get_class($environment) => $displayText]; })->sort(); $detectedClasses = []; @@ -314,8 +333,7 @@ private function selectCodeEnvironments(string $type, string $label): Collection )); foreach ($installedEnvNames as $envKey) { - $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name()) - ); + $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name())); if ($matchingEnv) { $detectedClasses[] = get_class($matchingEnv); } @@ -325,10 +343,15 @@ private function selectCodeEnvironments(string $type, string $label): Collection label: $label, options: $options->toArray(), default: array_unique($detectedClasses), - scroll: $type === 'ide' ? 5 : 4, - required: $type === 'ide', + scroll: $config['scroll'], + required: $config['required'], hint: empty($detectedClasses) ? null : sprintf('Auto-detected %s for you', - Arr::join(array_map(fn ($className) => $availableEnvironments->first(fn ($env) => get_class($env) === $className)->displayName(), $detectedClasses), ', ', ' & ') + Arr::join(array_map(function ($className) use ($availableEnvironments, $config) { + $env = $availableEnvironments->first(fn ($env) => get_class($env) === $className); + $displayMethod = $config['displayMethod']; + + return $env->{$displayMethod}(); + }, $detectedClasses), ', ', ' & ') ) ))->sort(); diff --git a/src/Install/CodeEnvironment/CodeEnvironment.php b/src/Install/CodeEnvironment/CodeEnvironment.php index 084a90f..579daa7 100644 --- a/src/Install/CodeEnvironment/CodeEnvironment.php +++ b/src/Install/CodeEnvironment/CodeEnvironment.php @@ -25,12 +25,12 @@ abstract public function displayName(): string; public function agentName(): ?string { - return $this->name(); + return $this->displayName(); } public function mcpClientName(): ?string { - return $this->name(); + return $this->displayName(); } /** diff --git a/src/Install/CodeEnvironment/PhpStorm.php b/src/Install/CodeEnvironment/PhpStorm.php index 34c3221..cd7e1a9 100644 --- a/src/Install/CodeEnvironment/PhpStorm.php +++ b/src/Install/CodeEnvironment/PhpStorm.php @@ -52,7 +52,7 @@ public function projectDetectionConfig(): array public function agentName(): string { - return 'junie'; + return 'Junie'; } public function mcpConfigPath(): string diff --git a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php index a9d88e5..3dc56db 100644 --- a/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php +++ b/tests/Unit/Install/CodeEnvironment/CodeEnvironmentTest.php @@ -122,16 +122,16 @@ public function mcpConfigPath(): string expect($result)->toBe(false); }); -test('agentName returns name by default', function () { +test('agentName returns displayName by default', function () { $environment = new TestCodeEnvironment($this->strategyFactory); - expect($environment->agentName())->toBe('test'); + expect($environment->agentName())->toBe('Test Environment'); }); -test('mcpClientName returns name by default', function () { +test('mcpClientName returns displayName by default', function () { $environment = new TestCodeEnvironment($this->strategyFactory); - expect($environment->mcpClientName())->toBe('test'); + expect($environment->mcpClientName())->toBe('Test Environment'); }); test('IsAgent returns true when implements Agent interface and has agentName', function () { From b62316541e51ab886c823b42458d20d8e1edf8b0 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Tue, 12 Aug 2025 23:35:46 +0530 Subject: [PATCH 11/16] refactor: streamline code environment detection and remove redundant method - Replaced `getAllPrograms` with `getCodeEnvironments` for consistency and public access. - Removed the private `getAllPrograms` method in favor of consolidated logic. - Adjusted usage across detection methods to align with the updated method structure. --- src/Install/CodeEnvironmentsDetector.php | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Install/CodeEnvironmentsDetector.php b/src/Install/CodeEnvironmentsDetector.php index f7581f1..5f74f4e 100644 --- a/src/Install/CodeEnvironmentsDetector.php +++ b/src/Install/CodeEnvironmentsDetector.php @@ -41,7 +41,7 @@ public function discoverSystemInstalledCodeEnvironments(): array { $platform = Platform::current(); - return $this->getAllPrograms() + return $this->getCodeEnvironments() ->filter(fn (CodeEnvironment $program) => $program->detectOnSystem($platform)) ->map(fn (CodeEnvironment $program) => $program->name()) ->values() @@ -55,7 +55,7 @@ public function discoverSystemInstalledCodeEnvironments(): array */ public function discoverProjectInstalledCodeEnvironments(string $basePath): array { - return $this->getAllPrograms() + return $this->getCodeEnvironments() ->filter(fn ($program) => $program->detectInProject($basePath)) ->map(fn ($program) => $program->name()) ->values() @@ -63,24 +63,12 @@ public function discoverProjectInstalledCodeEnvironments(string $basePath): arra } /** - * Get all registered programs. - * - * @return Collection - */ - private function getAllPrograms(): Collection - { - return collect($this->programs)->map( - fn (string $className) => $this->container->make($className) - ); - } - - /** - * Get all registered programs (public access). + * Get all registered code environments. * * @return Collection */ public function getCodeEnvironments(): Collection { - return $this->getAllPrograms(); + return collect($this->programs)->map(fn (string $className) => $this->container->make($className)); } } From 1dad67b5a7058fc772c675268cfcc70ef883f4cf Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:16:14 +0100 Subject: [PATCH 12/16] feat: update VS code display name --- src/Install/CodeEnvironment/VSCode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Install/CodeEnvironment/VSCode.php b/src/Install/CodeEnvironment/VSCode.php index f7ce3d9..97f891a 100644 --- a/src/Install/CodeEnvironment/VSCode.php +++ b/src/Install/CodeEnvironment/VSCode.php @@ -16,7 +16,7 @@ public function name(): string public function displayName(): string { - return 'Vs Code'; + return 'VS Code'; } public function systemDetectionConfig(Platform $platform): array From df817a1eaa2de82337fc78af0078beae2be2d327 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:16:26 +0100 Subject: [PATCH 13/16] feat: bypass 'what to install' if herd isn't available --- src/Console/InstallCommand.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index d65d959..039f771 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -225,20 +225,22 @@ private function selectBoostFeatures(): Collection { $defaultInstallOptions = ['mcp_server', 'ai_guidelines']; $installOptions = [ - 'mcp_server' => 'Boost MCP Server', - 'ai_guidelines' => 'Package AI Guidelines (i.e. Framework, Inertia, Pest)', + 'mcp_server' => 'Boost MCP Server (with 15+ tools)', + 'ai_guidelines' => 'Boost AI Guidelines (for Laravel, Inertia, and more)', ]; if ($this->herd->isMcpAvailable()) { $installOptions['herd_mcp'] = 'Herd MCP Server'; + + return collect(multiselect( + label: 'What shall we install?', + options: $installOptions, + default: $defaultInstallOptions, + required: true, + )); } - return collect(multiselect( - label: 'What shall we install?', - options: $installOptions, - default: $defaultInstallOptions, - required: true, - )); + return collect(['mcp_server', 'ai_guidelines']); } /** @@ -291,12 +293,11 @@ private function selectTargetAgents(): Collection /** * Get configuration settings for contract-specific selection behavior. * - * @param string $contractClass * @return array{scroll: int, required: bool, displayMethod: string} */ private function getSelectionConfig(string $contractClass): array { - return match($contractClass) { + return match ($contractClass) { Agent::class => ['scroll' => 4, 'required' => false, 'displayMethod' => 'agentName'], McpClient::class => ['scroll' => 5, 'required' => true, 'displayMethod' => 'displayName'], default => throw new InvalidArgumentException("Unsupported contract class: {$contractClass}"), From b28b3567ad775ec82fbeebb5697d58789282b841 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:32:01 +0100 Subject: [PATCH 14/16] feat: only show zed if it's installed --- src/Console/InstallCommand.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 039f771..421713f 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -173,10 +173,7 @@ private function outro(): void $guidelines[] = 'g:style'; } - // Combine all data $allData = array_merge($ideNames, $agentNames, $boostFeatures, $guidelines); - - // Create a compact CSV string and base64 encode $installData = base64_encode(implode(',', $allData)); $link = $this->hyperlink($label, 'https://boost.laravel.com/installed/?d='.$installData); @@ -320,12 +317,21 @@ private function selectCodeEnvironments(string $contractClass, string $label): C return collect(); } - $options = $availableEnvironments->mapWithKeys(function (CodeEnvironment $environment) use ($config) { - $displayMethod = $config['displayMethod']; - $displayText = $environment->{$displayMethod}(); + $options = $availableEnvironments + ->filter(function (CodeEnvironment $environment) { + // We only show Zed if it's actually installed + if ($environment->name() === 'zed' && ! in_array('zed', $this->systemInstalledCodeEnvironments)) { + return false; + } + + return true; + }) + ->mapWithKeys(function (CodeEnvironment $environment) use ($config) { + $displayMethod = $config['displayMethod']; + $displayText = $environment->{$displayMethod}(); - return [get_class($environment) => $displayText]; - })->sort(); + return [get_class($environment) => $displayText]; + })->sort(); $detectedClasses = []; $installedEnvNames = array_unique(array_merge( From 0cb33db06bfd888e77b97780f0bb6f0a50925e32 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:32:20 +0100 Subject: [PATCH 15/16] feat: slightly rename some main rule files when they're shown in the grid --- src/Install/GuidelineComposer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index 42ed8bd..81937cb 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -84,17 +84,17 @@ public function guidelines(): Collection protected function find(): Collection { $guidelines = collect(); - $guidelines->put('core', $this->guideline('core')); - $guidelines->put('boost/core', $this->guideline('boost/core')); + $guidelines->put('foundation', $this->guideline('foundation')); + $guidelines->put('boost', $this->guideline('boost/core')); - $guidelines->put('php/core', $this->guideline('php/core')); + $guidelines->put('php', $this->guideline('php/base')); // TODO: AI-48: Use composer target version, not PHP version. Production could be 8.1, but local is 8.4 // $phpMajorMinor = PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION; // $guidelines->put('php/v'.$phpMajorMinor, $this->guidelinesDir('php/'.$phpMajorMinor)); if (str_contains(config('app.url'), '.test') && $this->herd->isInstalled()) { - $guidelines->put('herd/core', $this->guideline('herd/core')); + $guidelines->put('herd', $this->guideline('herd/core')); } if ($this->config->laravelStyle) { From e69581a0dbc9d3a7f8455c8c9395e3065e42af68 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:46:51 +0100 Subject: [PATCH 16/16] feat: improve boost:install --- src/Console/InstallCommand.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 421713f..02b556b 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -341,7 +341,7 @@ private function selectCodeEnvironments(string $contractClass, string $label): C foreach ($installedEnvNames as $envKey) { $matchingEnv = $availableEnvironments->first(fn (CodeEnvironment $env) => strtolower($envKey) === strtolower($env->name())); - if ($matchingEnv) { + if ($matchingEnv && ($options->contains($matchingEnv->displayName() || $options->contains($matchingEnv->agentName())))) { $detectedClasses[] = get_class($matchingEnv); } } @@ -352,7 +352,7 @@ private function selectCodeEnvironments(string $contractClass, string $label): C default: array_unique($detectedClasses), scroll: $config['scroll'], required: $config['required'], - hint: empty($detectedClasses) ? null : sprintf('Auto-detected %s for you', + hint: empty($detectedClasses) ? '' : sprintf('Auto-detected %s for you', Arr::join(array_map(function ($className) use ($availableEnvironments, $config) { $env = $availableEnvironments->first(fn ($env) => get_class($env) === $className); $displayMethod = $config['displayMethod']; @@ -372,7 +372,7 @@ private function installGuidelines(): void } if ($this->selectedTargetAgents->isEmpty()) { - $this->info('No agents selected for guideline installation.'); + $this->info(' No agents selected for guideline installation.'); return; }