1818
1919use function Laravel \Prompts \intro ;
2020use function Laravel \Prompts \multiselect ;
21+ use function Laravel \Prompts \outro ;
2122use function Laravel \Prompts \select ;
2223use function Laravel \Prompts \text ;
2324
@@ -39,7 +40,8 @@ class InstallCommand extends Command
3940
4041 protected bool $ enforceTests = true ;
4142
42- protected array $ idesToInstallTo = ['other ' ];
43+ /** @var Collection<int, \Laravel\Boost\Contracts\Ide> */
44+ protected Collection $ idesToInstallTo ;
4345
4446 protected array $ boostToInstall = [];
4547
@@ -55,6 +57,7 @@ class InstallCommand extends Command
5557 public function handle (Roster $ roster ): void
5658 {
5759 $ this ->agentsToInstallTo = collect ();
60+ $ this ->idesToInstallTo = collect ();
5861 $ this ->roster = $ roster ;
5962 $ this ->colors = new class
6063 {
@@ -65,16 +68,18 @@ public function handle(Roster $roster): void
6568
6669 $ this ->intro ();
6770 $ this ->detect ();
68- // TODO: We see these packages installed, we have rules for X, so we'll add them
6971 $ this ->query ();
7072 $ this ->enact ();
73+ $ this ->outro ();
7174 }
7275
7376 protected function detect ()
7477 {
7578 $ this ->installedIdes = $ this ->detectInstalledIdes ();
7679 $ this ->detectedProjectIdes = $ this ->detectIdesUsedInProject ();
7780 // $this->detectedProjectAgents = $this->detectProjectAgents(); // TODO: Roo, Cline, Copilot
81+ // TODO: Should we create all agents to start, add a 'detected' prop to them that's set on construct
82+ // Maybe add a trait 'DetectsInstalled' and 'DetectsUsed' (in this project)
7883 }
7984
8085 protected function query ()
@@ -90,29 +95,26 @@ protected function query()
9095 $ this ->agentsToInstallTo = $ this ->agentsToInstallTo (); // AI Guidelines, which file do they go, are they separated, or all in one file?
9196 }
9297
93- protected function enact ()
98+ protected function enact (): void
9499 {
95100 if ($ this ->installingGuidelines () && ! empty ($ this ->agentsToInstallTo )) {
96- $ this ->enactGuidelines ($ this ->compose ());
101+ $ this ->enactGuidelines ($ this ->findGuidelines ());
97102 }
98103
99- if ($ this ->installingMcp () && ! empty ($ this ->idesToInstallTo )) {
100- echo "\ninstalling mcps now to: " ;
101- dump ($ this ->idesToInstallTo );
104+ if (($ this ->installingMcp () || $ this ->installingHerdMcp ()) && $ this ->idesToInstallTo ->isNotEmpty ()) {
105+ $ this ->enactMcpServers ();
102106 }
103107
104- if ($ this ->installingHerdMcp () && ! empty ($ this ->idesToInstallTo )) {
105- echo "\ninstalling herd mcp now to: " ;
106- dump ($ this ->idesToInstallTo );
107- }
108+ // Check if any of the selected IDEs is an "other" type IDE
109+ $ hasOtherIde = true ;
108110
109- if (in_array ( ' other ' , $ this -> idesToInstallTo ) ) {
111+ if ($ hasOtherIde ) {
110112 $ this ->newLine ();
111113 $ this ->line ('Add to your mcp file: ./artisan boost:mcp ' ); // some ides require absolute
112114 }
113115 }
114116
115- protected function compose (): string
117+ protected function findGuidelines (): Collection
116118 {
117119 // TODO: Just move to blade views and compact public properties?
118120 $ composed = collect (['core ' => $ this ->guideline ('core.md ' , [
@@ -132,14 +134,11 @@ protected function compose(): string
132134 }
133135
134136 // Add all core.md and version specific docs for Roster supported packages
135- // We don't add guidelines for packages not supported by Roster right now
137+ // We don't add guidelines for packages unsupported by Roster right now
136138 foreach ($ this ->roster ->packages () as $ package ) {
137139 $ guidelineDir = str_replace ('_ ' , '- ' , strtolower ($ package ->name ()));
138- $ coreGuidelines = $ this ->guideline ($ guidelineDir .'/core.md ' ); // Add core
139- if ($ coreGuidelines ) {
140- $ composed ->put ($ guidelineDir .'/core ' , $ coreGuidelines );
141- }
142140
141+ $ composed ->put ($ guidelineDir .'/core ' , $ this ->guideline ($ guidelineDir .'/core.md ' )); // Add core
143142 $ composed ->put (
144143 $ guidelineDir .'/v ' .$ package ->majorVersion (),
145144 $ this ->guidelines ($ guidelineDir .'/ ' .$ package ->majorVersion ())
@@ -150,7 +149,14 @@ protected function compose(): string
150149 $ composed ->put ('tests ' , $ this ->guideline ('enforce-tests.md ' ));
151150 }
152151
153- return $ composed ->whereNotNull ()->map (fn ($ content , $ key ) => "# {$ key }\n{$ content }\n" )
152+ return $ composed
153+ ->whereNotNull ();
154+ }
155+
156+ protected function compose (Collection $ composed ): string
157+ {
158+ return $ composed
159+ ->map (fn ($ content , $ key ) => "# {$ key }\n{$ content }\n" )
154160 ->join ("\n\n==== \n\n" );
155161 }
156162
@@ -240,7 +246,7 @@ protected function detectIdesUsedInProject(): array
240246 }
241247
242248 if (file_exists (base_path ('CLAUDE.md ' )) || is_dir (base_path ('.claude ' ))) {
243- $ detected [] = 'claude_code ' ;
249+ $ detected [] = 'claudecode ' ;
244250 }
245251
246252 $ detected [] = 'other ' ;
@@ -294,14 +300,19 @@ protected function isHerdInstalled(): bool
294300 }
295301
296302 protected function isHerdMCPAvailable (): bool
303+ {
304+ return file_exists ($ this ->herdMcpPath ());
305+ }
306+
307+ protected function herdMcpPath (): string
297308 {
298309 $ isWindows = PHP_OS_FAMILY === 'Windows ' ;
299310
300311 if ($ isWindows ) {
301- return file_exists ( $ this ->getHomePath ().'/.config/herd/bin/herd-mcp.phar ' ) ;
312+ return $ this ->getHomePath ().'/.config/herd/bin/herd-mcp.phar ' ;
302313 }
303314
304- return file_exists ( $ this ->getHomePath ().'/Library/Application Support/Herd/bin/herd-mcp.phar ' ) ;
315+ return $ this ->getHomePath ().'/Library/Application Support/Herd/bin/herd-mcp.phar ' ;
305316 }
306317
307318 /*
@@ -331,6 +342,11 @@ private function intro()
331342 $ this ->line (' Let \'s give ' .$ this ->colors ->bgYellow ($ this ->colors ->black ($ this ->projectName )).' a Boost ' );
332343 }
333344
345+ private function outro ()
346+ {
347+ outro ('All done. Enjoy the boost 🚀 ' );
348+ }
349+
334350 protected function projectPurpose (): string
335351 {
336352 return text (
@@ -365,30 +381,6 @@ protected function shouldEnforceTests(bool $ask = true): bool
365381 return $ enforce ;
366382 }
367383
368- protected function idesToInstallTo (): array
369- {
370- // Limit our surface area for launch. We can support more after
371- $ ideOptions = [
372- 'claude_code ' => 'Claude Code ' ,
373- 'cursor ' => 'Cursor ' ,
374- 'phpstorm ' => 'PHPStorm ' ,
375- 'vscode ' => 'VSCode ' ,
376- 'other ' => 'Other ' ,
377- ];
378-
379- // Tell API which ones?
380- $ autoDetectedIdesString = Arr::join (array_map (fn (string $ ideKey ) => $ ideOptions [$ ideKey ] ?? '' , $ this ->detectedProjectIdes ), ', ' , ' & ' );
381-
382- return multiselect (
383- label: sprintf ('Which IDEs do you use in %s? (space to select) ' , $ this ->projectName ),
384- options: $ ideOptions ,
385- default: $ this ->detectedProjectIdes ,
386- scroll: 5 ,
387- required: true ,
388- hint: sprintf ('Auto-detected %s for you ' , $ autoDetectedIdesString )
389- );
390- }
391-
392384 protected function boostToInstall (): array
393385 {
394386 $ defaultToInstallOptions = ['mcp_server ' , 'ai_guidelines ' ];
@@ -427,6 +419,61 @@ protected function detectProjectAgents(): array
427419 return [];
428420 }
429421
422+ /**
423+ * @return Collection<int, \Laravel\Boost\Contracts\Ide>
424+ */
425+ protected function idesToInstallTo (): Collection
426+ {
427+ $ ides = [];
428+ if (! $ this ->installingMcp () && ! $ this ->installingHerdMcp ()) {
429+ return collect ();
430+ }
431+
432+ $ agentDir = implode (DIRECTORY_SEPARATOR , [__DIR__ , '.. ' , 'Install ' , 'Agents ' ]);
433+
434+ $ finder = Finder::create ()
435+ ->in ($ agentDir )
436+ ->files ()
437+ ->name ('*.php ' );
438+
439+ foreach ($ finder as $ ideFile ) {
440+ $ className = 'Laravel \\Boost \\Install \\Agents \\' .$ ideFile ->getBasename ('.php ' );
441+
442+ if (class_exists ($ className )) {
443+ $ reflection = new \ReflectionClass ($ className );
444+
445+ if ($ reflection ->implementsInterface (\Laravel \Boost \Contracts \Ide::class) && ! $ reflection ->isAbstract ()) {
446+ $ ides [$ className ] = Str::headline ($ ideFile ->getBasename ('.php ' ));
447+ }
448+ }
449+ }
450+
451+ ksort ($ ides );
452+ // $ides['other'] = 'Other'; // TODO: Make 'Other' work now we are working with classes not strings
453+
454+ // Map detected IDE keys to class names
455+ $ detectedClasses = [];
456+ foreach ($ this ->detectedProjectIdes as $ ideKey ) {
457+ foreach ($ ides as $ className => $ displayName ) {
458+ if (strtolower ($ ideKey ) === strtolower (class_basename ($ className ))) {
459+ $ detectedClasses [] = $ className ;
460+ break ;
461+ }
462+ }
463+ }
464+
465+ $ selectedIdeClasses = collect (multiselect (
466+ label: sprintf ('Which IDEs do you use in %s? (space to select) ' , $ this ->projectName ),
467+ options: $ ides ,
468+ default: $ detectedClasses ,
469+ scroll: 5 ,
470+ required: true ,
471+ hint: sprintf ('Auto-detected %s for you ' , Arr::join (array_map (fn ($ c ) => class_basename ($ c ), $ detectedClasses ), ', ' , ' & ' ))
472+ ));
473+
474+ return $ selectedIdeClasses ->map (fn ($ ideClass ) => new $ ideClass );
475+ }
476+
430477 /**
431478 * @return Collection<int, \Laravel\Boost\Contracts\Agent>
432479 */
@@ -468,7 +515,7 @@ protected function agentsToInstallTo(): Collection
468515 return $ selectedAgentClasses ->map (fn ($ agentClass ) => new $ agentClass );
469516 }
470517
471- protected function enactGuidelines (string $ composedAiGuidelines ): void
518+ protected function enactGuidelines (Collection $ composed ): void
472519 {
473520 if (! $ this ->installingGuidelines ()) {
474521 return ;
@@ -481,11 +528,13 @@ protected function enactGuidelines(string $composedAiGuidelines): void
481528 }
482529
483530 $ this ->newLine ();
484- $ this ->info ('Installing AI guidelines to selected agents... ' );
531+ $ this ->info (sprintf ('Found %d guidelines and adding to your selected agents ' , $ composed ->count ()));
532+ $ this ->line ($ composed ->keys ()->join (', ' , ' & ' ));
485533 $ this ->newLine ();
486534
487535 $ successful = [];
488536 $ failed = [];
537+ $ composedAiGuidelines = $ this ->compose ($ composed );
489538
490539 foreach ($ this ->agentsToInstallTo as $ agent ) {
491540 $ agentName = class_basename ($ agent );
@@ -505,13 +554,6 @@ protected function enactGuidelines(string $composedAiGuidelines): void
505554
506555 $ this ->newLine ();
507556
508- if (count ($ successful ) > 0 ) {
509- $ this ->info (sprintf ('✓ Successfully installed guidelines to %d agent%s ' ,
510- count ($ successful ),
511- count ($ successful ) === 1 ? '' : 's '
512- ));
513- }
514-
515557 if (count ($ failed ) > 0 ) {
516558 $ this ->error (sprintf ('✗ Failed to install guidelines to %d agent%s: ' ,
517559 count ($ failed ),
@@ -542,4 +584,66 @@ protected function installingHerdMcp(): bool
542584 {
543585 return in_array ('herd_mcp ' , $ this ->boostToInstall , true );
544586 }
587+
588+ protected function enactMcpServers (): void
589+ {
590+ $ this ->newLine ();
591+ $ this ->info ('Installing MCP servers to your selected IDEs ' );
592+ $ this ->newLine ();
593+
594+ $ failed = [];
595+
596+ foreach ($ this ->idesToInstallTo as $ ide ) {
597+ $ ideName = class_basename ($ ide );
598+ $ this ->output ->write (" {$ ideName }... " );
599+ $ results = [];
600+
601+ // Install Laravel Boost MCP if enabled
602+ if ($ this ->installingMcp ()) {
603+ try {
604+ $ result = $ ide ->installMcp ('laravel-boost ' , base_path ('artisan ' ), ['boost:mcp ' ]);
605+
606+ if ($ result ) {
607+ $ results [] = '✓ Boost ' ;
608+ } else {
609+ $ results [] = '✗ Boost ' ;
610+ $ failed [$ ideName ]['boost ' ] = 'Failed to write configuration ' ;
611+ }
612+ } catch (\Exception $ e ) {
613+ $ results [] = '✗ Boost ' ;
614+ $ failed [$ ideName ]['boost ' ] = $ e ->getMessage ();
615+ }
616+ }
617+
618+ // Install Herd MCP if enabled
619+ if ($ this ->installingHerdMcp ()) {
620+ try {
621+ $ result = $ ide ->installMcp ('herd ' , PHP_BINARY , [$ this ->herdMcpPath ()]);
622+
623+ if ($ result ) {
624+ $ results [] = '✓ Herd ' ;
625+ } else {
626+ $ results [] = '✗ Herd ' ;
627+ $ failed [$ ideName ]['herd ' ] = 'Failed to write configuration ' ;
628+ }
629+ } catch (\Exception $ e ) {
630+ $ results [] = '✗ Herd ' ;
631+ $ failed [$ ideName ]['herd ' ] = $ e ->getMessage ();
632+ }
633+ }
634+
635+ $ this ->line (implode (' ' , $ results ));
636+ }
637+
638+ $ this ->newLine ();
639+
640+ if (count ($ failed ) > 0 ) {
641+ $ this ->error (sprintf ('✗ Some MCP servers failed to install: ' ));
642+ foreach ($ failed as $ ideName => $ errors ) {
643+ foreach ($ errors as $ server => $ error ) {
644+ $ this ->line (" - {$ ideName } ( {$ server }): {$ error }" );
645+ }
646+ }
647+ }
648+ }
545649}
0 commit comments