99use Illuminate \Support \Arr ;
1010use Illuminate \Support \Collection ;
1111use Illuminate \Support \Str ;
12- use Laravel \Boost \Contracts \Agent ;
13- use Laravel \Boost \Contracts \Ide ;
1412use Laravel \Boost \Install \Cli \DisplayHelper ;
13+ use Laravel \Boost \Install \CodeEnvironment \CodeEnvironment ;
1514use Laravel \Boost \Install \CodeEnvironmentsDetector ;
1615use Laravel \Boost \Install \GuidelineComposer ;
1716use Laravel \Boost \Install \GuidelineConfig ;
1817use Laravel \Boost \Install \GuidelineWriter ;
1918use Laravel \Boost \Install \Herd ;
2019use Laravel \Prompts \Concerns \Colors ;
2120use Laravel \Prompts \Terminal ;
22- use ReflectionClass ;
2321use Symfony \Component \Console \Attribute \AsCommand ;
2422use Symfony \Component \Finder \Finder ;
2523
@@ -39,10 +37,10 @@ class InstallCommand extends Command
3937
4038 private Terminal $ terminal ;
4139
42- /** @var Collection<int, Agent > */
40+ /** @var Collection<int, CodeEnvironment > */
4341 private Collection $ selectedTargetAgents ;
4442
45- /** @var Collection<int, Ide > */
43+ /** @var Collection<int, CodeEnvironment > */
4644 private Collection $ selectedTargetIdes ;
4745
4846 /** @var Collection<int, string> */
@@ -57,8 +55,6 @@ class InstallCommand extends Command
5755
5856 private bool $ enforceTests = true ;
5957
60- private array $ projectInstalledAgents = [];
61-
6258 private string $ greenTick ;
6359
6460 private string $ redCross ;
@@ -114,7 +110,6 @@ private function discoverEnvironment(): void
114110 {
115111 $ this ->systemInstalledCodeEnvironments = $ this ->codeEnvironmentsDetector ->discoverSystemInstalledCodeEnvironments ();
116112 $ this ->projectInstalledCodeEnvironments = $ this ->codeEnvironmentsDetector ->discoverProjectInstalledCodeEnvironments (base_path ());
117- $ this ->projectInstalledAgents = $ this ->discoverProjectAgents ();
118113 }
119114
120115 private function collectInstallationPreferences (): void
@@ -127,7 +122,7 @@ private function collectInstallationPreferences(): void
127122
128123 private function enact (): void
129124 {
130- if ($ this ->shouldInstallAiGuidelines () && ! empty ( $ this ->selectedTargetAgents )) {
125+ if ($ this ->shouldInstallAiGuidelines () && $ this ->selectedTargetAgents -> isNotEmpty ( )) {
131126 $ this ->enactGuidelines ();
132127 }
133128
@@ -163,8 +158,8 @@ private function outro(): void
163158 {
164159 $ label = 'https://boost.laravel.com/installed ' ;
165160
166- $ ideNames = $ this ->selectedTargetIdes ->map (fn ($ ide ) => 'i: ' .class_basename ( $ ide ))->toArray ();
167- $ agentNames = $ this ->selectedTargetAgents ->map (fn ($ agent ) => 'a: ' .class_basename ( $ agent ))->toArray ();
161+ $ ideNames = $ this ->selectedTargetIdes ->map (fn ($ ide ) => 'i: ' .$ ide-> ideName ( ))->toArray ();
162+ $ agentNames = $ this ->selectedTargetAgents ->map (fn ($ agent ) => 'a: ' .$ agent-> agentName ( ))->toArray ();
168163 $ boostFeatures = $ this ->selectedBoostFeatures ->map (fn ($ feature ) => 'b: ' .$ feature )->toArray ();
169164
170165 // Guidelines installed (prefix: g)
@@ -261,137 +256,86 @@ protected function boostToolsToDisable(): array
261256 /**
262257 * @return array<int, string>
263258 */
264- private function discoverProjectAgents (): array
265- {
266- $ agents = [];
267- $ projectAgents = $ this ->codeEnvironmentsDetector ->discoverProjectInstalledCodeEnvironments (base_path ());
268-
269- // Map IDE detections to their corresponding agents
270- $ ideToAgentMap = [
271- 'phpstorm ' => 'junie ' ,
272- 'claudecode ' => 'claudecode ' ,
273- 'cursor ' => 'cursor ' ,
274- 'copilot ' => 'copilot ' ,
275- ];
276-
277- foreach ($ projectAgents as $ app ) {
278- if (isset ($ ideToAgentMap [$ app ])) {
279- $ agents [] = $ ideToAgentMap [$ app ];
280- }
281- }
282-
283- foreach ($ this ->systemInstalledCodeEnvironments as $ ide ) {
284- if (isset ($ ideToAgentMap [$ ide ]) && ! in_array ($ ideToAgentMap [$ ide ], $ agents )) {
285- $ agents [] = $ ideToAgentMap [$ ide ];
286- }
287- }
288-
289- return array_unique ($ agents );
290- }
291259
292260 /**
293- * @return Collection<int, Ide >
261+ * @return Collection<int, CodeEnvironment >
294262 */
295263 private function selectTargetIdes (): Collection
296264 {
297- $ ides = [];
298265 if (! $ this ->shouldInstallMcp () && ! $ this ->shouldInstallHerdMcp ()) {
299266 return collect ();
300267 }
301268
302- $ agentDir = implode (DIRECTORY_SEPARATOR , [__DIR__ , '.. ' , 'Install ' , 'Agents ' ]);
303-
304- $ finder = Finder::create ()
305- ->in ($ agentDir )
306- ->files ()
307- ->name ('*.php ' );
308-
309- foreach ($ finder as $ ideFile ) {
310- $ className = 'Laravel \\Boost \\Install \\Agents \\' .$ ideFile ->getBasename ('.php ' );
311-
312- if (class_exists ($ className )) {
313- $ reflection = new ReflectionClass ($ className );
314-
315- if ($ reflection ->implementsInterface (Ide::class) && ! $ reflection ->isAbstract ()) {
316- $ ides [$ className ] = Str::headline ($ ideFile ->getBasename ('.php ' ));
317- }
318- }
319- }
320-
321- ksort ($ ides );
322-
323- $ detectedClasses = [];
324- foreach ($ this ->projectInstalledCodeEnvironments as $ ideKey ) {
325- foreach ($ ides as $ className => $ displayName ) {
326- if (strtolower ($ ideKey ) === strtolower (class_basename ($ className ))) {
327- $ detectedClasses [] = $ className ;
328- break ;
329- }
330- }
331- }
332-
333- $ selectedIdeClasses = collect (multiselect (
334- label: sprintf ('Which code editors do you use in %s? ' , $ this ->projectName ),
335- options: $ ides ,
336- default: $ detectedClasses ,
337- scroll: 5 ,
338- required: true ,
339- hint: sprintf ('Auto-detected %s for you ' , Arr::join (array_map (fn ($ c ) => class_basename ($ c ), $ detectedClasses ), ', ' , ' & ' ))
340- ))->sort ();
341-
342- return $ selectedIdeClasses ->map (fn ($ ideClass ) => new $ ideClass );
269+ return $ this ->selectCodeEnvironments (
270+ 'ide ' ,
271+ sprintf ('Which code editors do you use in %s? ' , $ this ->projectName )
272+ );
343273 }
344274
345275 /**
346- * @return Collection<int, Agent >
276+ * @return Collection<int, CodeEnvironment >
347277 */
348278 private function selectTargetAgents (): Collection
349279 {
350- $ agents = [];
351280 if (! $ this ->shouldInstallAiGuidelines ()) {
352281 return collect ();
353282 }
354283
355- $ agentDir = implode (DIRECTORY_SEPARATOR , [__DIR__ , '.. ' , 'Install ' , 'Agents ' ]);
356-
357- $ finder = Finder::create ()
358- ->in ($ agentDir )
359- ->files ()
360- ->name ('*.php ' );
284+ return $ this ->selectCodeEnvironments (
285+ 'agent ' ,
286+ sprintf ('Which agents need AI guidelines for %s? ' , $ this ->projectName )
287+ );
288+ }
361289
362- foreach ($ finder as $ agentFile ) {
363- $ className = 'Laravel \\Boost \\Install \\Agents \\' .$ agentFile ->getBasename ('.php ' );
290+ /**
291+ * @return Collection<int, CodeEnvironment>
292+ */
293+ private function selectCodeEnvironments (string $ type , string $ label ): Collection
294+ {
295+ $ allEnvironments = $ this ->codeEnvironmentsDetector ->getCodeEnvironments ();
364296
365- if (class_exists ($ className )) {
366- $ reflection = new ReflectionClass ($ className );
297+ // Filter by type capability
298+ $ availableEnvironments = $ allEnvironments ->filter (function (CodeEnvironment $ environment ) use ($ type ) {
299+ return ($ type === 'ide ' && $ environment ->supportsIde ()) ||
300+ ($ type === 'agent ' && $ environment ->supportsAgent ());
301+ });
367302
368- if ($ reflection ->implementsInterface (Agent::class)) {
369- $ agents [$ className ] = Str::headline ($ agentFile ->getBasename ('.php ' ));
370- }
371- }
303+ if ($ availableEnvironments ->isEmpty ()) {
304+ return collect ();
372305 }
373306
374- ksort ($ agents );
307+ // Build options for multiselect
308+ $ options = $ availableEnvironments ->mapWithKeys (function (CodeEnvironment $ environment ) {
309+ return [get_class ($ environment ) => $ environment ->displayName ()];
310+ })->sort ();
375311
376- // Map detected agent keys to class names
312+ // Auto-detect installed environments
377313 $ detectedClasses = [];
378- foreach ($ this ->projectInstalledAgents as $ agentKey ) {
379- foreach ($ agents as $ className => $ displayName ) {
380- if (strtolower ($ agentKey ) === strtolower (class_basename ($ className ))) {
381- $ detectedClasses [] = $ className ;
382- break ;
383- }
314+ $ installedEnvNames = array_unique (array_merge (
315+ $ this ->projectInstalledCodeEnvironments ,
316+ $ this ->systemInstalledCodeEnvironments
317+ ));
318+
319+ foreach ($ installedEnvNames as $ envKey ) {
320+ $ matchingEnv = $ availableEnvironments ->first (fn (CodeEnvironment $ env ) => strtolower ($ envKey ) === strtolower ($ env ->name ())
321+ );
322+ if ($ matchingEnv ) {
323+ $ detectedClasses [] = get_class ($ matchingEnv );
384324 }
385325 }
386326
387- $ selectedAgentClasses = collect (multiselect (
388- label: sprintf ('Which agents need AI guidelines for %s? ' , $ this ->projectName ),
389- options: $ agents ,
390- default: $ detectedClasses ,
391- scroll: 4 ,
327+ $ selectedClasses = collect (multiselect (
328+ label: $ label ,
329+ options: $ options ->toArray (),
330+ default: array_unique ($ detectedClasses ),
331+ scroll: $ type === 'ide ' ? 5 : 4 ,
332+ required: $ type === 'ide ' ,
333+ hint: empty ($ detectedClasses ) ? null : sprintf ('Auto-detected %s for you ' ,
334+ Arr::join (array_map (fn ($ className ) => $ availableEnvironments ->first (fn ($ env ) => get_class ($ env ) === $ className )->displayName (), $ detectedClasses ), ', ' , ' & ' )
335+ )
392336 ))->sort ();
393337
394- return $ selectedAgentClasses ->map (fn ($ agentClass ) => new $ agentClass );
338+ return $ selectedClasses ->map (fn ($ className ) => $ availableEnvironments -> first ( fn ( $ env ) => get_class ( $ env ) === $ className ) );
395339 }
396340
397341 protected function enactGuidelines (): void
@@ -424,9 +368,9 @@ protected function enactGuidelines(): void
424368 $ failed = [];
425369 $ composedAiGuidelines = $ composer ->compose ();
426370
427- $ longestAgentName = max (1 , ...$ this ->selectedTargetAgents ->map (fn ($ agent ) => Str::length (class_basename ( $ agent )))->toArray ());
371+ $ longestAgentName = max (1 , ...$ this ->selectedTargetAgents ->map (fn ($ agent ) => Str::length ($ agent-> agentName ( )))->toArray ());
428372 foreach ($ this ->selectedTargetAgents as $ agent ) {
429- $ agentName = class_basename ( $ agent );
373+ $ agentName = $ agent-> agentName ( );
430374 $ displayAgentName = str_pad ($ agentName , $ longestAgentName );
431375 $ this ->output ->write (" {$ displayAgentName }... " );
432376
@@ -483,10 +427,10 @@ private function enactMcpServers(): void
483427 usleep (750000 );
484428
485429 $ failed = [];
486- $ longestIdeName = max (1 , ...$ this ->selectedTargetIdes ->map (fn ($ ide ) => Str::length (class_basename ( $ ide )))->toArray ());
430+ $ longestIdeName = max (1 , ...$ this ->selectedTargetIdes ->map (fn ($ ide ) => Str::length ($ ide-> ideName ( )))->toArray ());
487431
488432 foreach ($ this ->selectedTargetIdes as $ ide ) {
489- $ ideName = class_basename ( $ ide );
433+ $ ideName = $ ide-> ideName ( );
490434 $ ideDisplay = str_pad ($ ideName , $ longestIdeName );
491435 $ this ->output ->write (" {$ ideDisplay }... " );
492436 $ results = [];
0 commit comments