1010use Illuminate \Support \Facades \Process ;
1111use Illuminate \Support \Str ;
1212use Laravel \Prompts \Concerns \Colors ;
13+ use Laravel \Prompts \Terminal ;
1314use Laravel \Roster \Enums \Packages ;
1415use Laravel \Roster \Roster ;
1516use Symfony \Component \Console \Attribute \AsCommand ;
@@ -54,15 +55,25 @@ class InstallCommand extends Command
5455
5556 protected Roster $ roster ;
5657
58+ private string $ greenTick ;
59+
60+ private string $ redCross ;
61+
62+ private Terminal $ terminal ;
63+
5764 public function handle (Roster $ roster ): void
5865 {
66+ $ this ->terminal = new Terminal ;
67+ $ this ->terminal ->initDimensions ();
5968 $ this ->agentsToInstallTo = collect ();
6069 $ this ->idesToInstallTo = collect ();
6170 $ this ->roster = $ roster ;
6271 $ this ->colors = new class
6372 {
6473 use Colors;
6574 };
75+ $ this ->greenTick = $ this ->colors ->green ('✓ ' );
76+ $ this ->redCross = $ this ->colors ->red ('✗ ' );
6677
6778 $ this ->projectName = basename (base_path ());
6879
@@ -110,7 +121,8 @@ protected function enact(): void
110121
111122 if ($ hasOtherIde ) {
112123 $ this ->newLine ();
113- $ this ->line ('Add to your mcp file: ./artisan boost:mcp ' ); // some ides require absolute
124+ $ this ->line ('Add Boost MCP manually if needed: ' ); // some ides require absolute
125+ $ this ->datatable ([['Command ' , base_path ('artisan ' )], ['Args ' , 'boost:mcp ' ]]);
114126 }
115127 }
116128
@@ -329,22 +341,26 @@ protected function herdMcpPath(): string
329341 private function intro ()
330342 {
331343 $ this ->newline ();
332- $ this ->line (<<<'HEADER'
333- ██████╗ ██████╗ ██████╗ ███████╗ ████████╗
334- ██╔══██╗ ██╔═══██╗ ██╔═══██╗ ██╔════╝ ╚══██╔══╝
335- ██████╔╝ ██║ ██║ ██║ ██║ ███████╗ ██║
336- ██╔══██╗ ██║ ██║ ██║ ██║ ╚════██║ ██║
337- ██████╔╝ ╚██████╔╝ ╚██████╔╝ ███████║ ██║
338- ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝
339- HEADER
340- );
344+ $ header = <<<HEADER
345+ \e[38;2;234;36;16m██████╗ ██████╗ ██████╗ ███████╗ ████████╗
346+ \e[38;2;234;42;22m██╔══██╗ ██╔═══██╗ ██╔═══██╗ ██╔════╝ ╚══██╔══╝
347+ \e[38;2;234;48;28m██████╔╝ ██║ ██║ ██║ ██║ ███████╗ ██║
348+ \e[38;2;234;54;34m██╔══██╗ ██║ ██║ ██║ ██║ ╚════██║ ██║
349+ \e[38;2;234;60;40m██████╔╝ ╚██████╔╝ ╚██████╔╝ ███████║ ██║
350+ \e[38;2;234;66;46m╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ \e[0m
351+ HEADER ;
352+ foreach (explode (PHP_EOL , $ header ) as $ i => $ line ) {
353+ echo "{$ line }\n" ;
354+ }
355+
341356 intro ('✦ Laravel Boost :: Install :: We Must Ship ✦ ' );
342357 $ this ->line (' Let \'s give ' .$ this ->colors ->bgYellow ($ this ->colors ->black ($ this ->projectName )).' a Boost ' );
343358 }
344359
345360 private function outro ()
346361 {
347362 outro ('All done. Enjoy the boost 🚀 ' );
363+ outro ('Get the most out of Boost by visiting https://boost.laravel.com/installed ' );
348364 }
349365
350366 protected function projectPurpose (): string
@@ -528,27 +544,29 @@ protected function enactGuidelines(Collection $composed): void
528544 }
529545
530546 $ this ->newLine ();
531- $ this ->info (sprintf ('Found %d guidelines and adding to your selected agents ' , $ composed ->count ()));
532- $ this ->line ($ composed ->keys ()->join ( ' , ' , ' & ' ));
547+ $ this ->info (sprintf ('Adding %d guidelines to your selected agents ' , $ composed ->count ()));
548+ $ this ->grid ($ composed ->keys ()->toArray ( ));
533549 $ this ->newLine ();
534550
535551 $ successful = [];
536552 $ failed = [];
537553 $ composedAiGuidelines = $ this ->compose ($ composed );
538554
555+ $ longestAgentName = max (1 , ...$ this ->agentsToInstallTo ->map (fn ($ agent ) => Str::length (class_basename ($ agent )))->toArray ());
539556 foreach ($ this ->agentsToInstallTo as $ agent ) {
540557 $ agentName = class_basename ($ agent );
541- $ this ->output ->write (" {$ agentName }... " );
558+ $ displayAgentName = str_pad ($ agentName , $ longestAgentName , ' ' , STR_PAD_RIGHT );
559+ $ this ->output ->write (" {$ displayAgentName }... " );
542560
543561 try {
544562 $ guidelineWriter = new \Laravel \Boost \Install \GuidelineWriter ($ agent );
545563 $ guidelineWriter ->write ($ composedAiGuidelines );
546564
547565 $ successful [] = $ agentName ;
548- $ this ->line (' ✓ ' );
566+ $ this ->line ($ this -> greenTick );
549567 } catch (\Exception $ e ) {
550568 $ failed [$ agentName ] = $ e ->getMessage ();
551- $ this ->line (' ✗ ' );
569+ $ this ->line ($ this -> redCross );
552570 }
553571 }
554572
@@ -585,32 +603,208 @@ protected function installingHerdMcp(): bool
585603 return in_array ('herd_mcp ' , $ this ->boostToInstall , true );
586604 }
587605
606+ protected function datatable (array $ data ): void
607+ {
608+ if (empty ($ data )) {
609+ return ;
610+ }
611+
612+ // Calculate column widths
613+ $ columnWidths = [];
614+ foreach ($ data as $ row ) {
615+ $ colIndex = 0 ;
616+ foreach ($ row as $ cell ) {
617+ $ length = mb_strlen ((string ) $ cell );
618+ if (! isset ($ columnWidths [$ colIndex ]) || $ length > $ columnWidths [$ colIndex ]) {
619+ $ columnWidths [$ colIndex ] = $ length ;
620+ }
621+ $ colIndex ++;
622+ }
623+ }
624+
625+ // Add padding
626+ $ columnWidths = array_map (fn ($ width ) => $ width + 2 , $ columnWidths );
627+
628+ // Unicode box drawing characters
629+ $ topLeft = '╭ ' ;
630+ $ topRight = '╮ ' ;
631+ $ bottomLeft = '╰ ' ;
632+ $ bottomRight = '╯ ' ;
633+ $ horizontal = '─ ' ;
634+ $ vertical = '│ ' ;
635+ $ cross = '┼ ' ;
636+ $ topT = '┬ ' ;
637+ $ bottomT = '┴ ' ;
638+ $ leftT = '├ ' ;
639+ $ rightT = '┤ ' ;
640+
641+ // Draw top border
642+ $ topBorder = $ topLeft ;
643+ foreach ($ columnWidths as $ index => $ width ) {
644+ $ topBorder .= str_repeat ($ horizontal , $ width );
645+ if ($ index < count ($ columnWidths ) - 1 ) {
646+ $ topBorder .= $ topT ;
647+ }
648+ }
649+ $ topBorder .= $ topRight ;
650+ $ this ->line ($ topBorder );
651+
652+ // Draw rows
653+ $ rowCount = 0 ;
654+ foreach ($ data as $ row ) {
655+ $ line = $ vertical ;
656+ $ colIndex = 0 ;
657+ foreach ($ row as $ cell ) {
658+ $ cellStr = ($ colIndex === 0 ) ? "\e[1m " .$ cell ."\e[0m " : $ cell ;
659+ $ padding = $ columnWidths [$ colIndex ] - mb_strlen ($ cell );
660+ $ line .= ' ' .$ cellStr .str_repeat (' ' , $ padding - 1 ).$ vertical ;
661+ $ colIndex ++;
662+ }
663+ $ this ->line ($ line );
664+
665+ // Draw separator between rows (except after last row)
666+ if ($ rowCount < count ($ data ) - 1 ) {
667+ $ separator = $ leftT ;
668+ foreach ($ columnWidths as $ index => $ width ) {
669+ $ separator .= str_repeat ($ horizontal , $ width );
670+ if ($ index < count ($ columnWidths ) - 1 ) {
671+ $ separator .= $ cross ;
672+ }
673+ }
674+ $ separator .= $ rightT ;
675+ $ this ->line ($ separator );
676+ }
677+ $ rowCount ++;
678+ }
679+
680+ // Draw bottom border
681+ $ bottomBorder = $ bottomLeft ;
682+ foreach ($ columnWidths as $ index => $ width ) {
683+ $ bottomBorder .= str_repeat ($ horizontal , $ width );
684+ if ($ index < count ($ columnWidths ) - 1 ) {
685+ $ bottomBorder .= $ bottomT ;
686+ }
687+ }
688+ $ bottomBorder .= $ bottomRight ;
689+ $ this ->line ($ bottomBorder );
690+ }
691+
692+ protected function grid (array $ items ): void
693+ {
694+ if (empty ($ items )) {
695+ return ;
696+ }
697+
698+ // Get terminal width
699+ $ terminalWidth = $ this ->terminal ->cols () ?? 80 ;
700+
701+ // Calculate the longest item length
702+ $ maxItemLength = max (array_map ('mb_strlen ' , $ items ));
703+
704+ // Add padding (2 spaces on each side + 1 for border)
705+ $ cellWidth = $ maxItemLength + 4 ;
706+
707+ // Calculate how many cells can fit per row
708+ $ cellsPerRow = max (1 , (int ) floor (($ terminalWidth - 1 ) / ($ cellWidth + 1 )));
709+
710+ // Unicode box drawing characters
711+ $ topLeft = '╭ ' ;
712+ $ topRight = '╮ ' ;
713+ $ bottomLeft = '╰ ' ;
714+ $ bottomRight = '╯ ' ;
715+ $ horizontal = '─ ' ;
716+ $ vertical = '│ ' ;
717+ $ cross = '┼ ' ;
718+ $ topT = '┬ ' ;
719+ $ bottomT = '┴ ' ;
720+ $ leftT = '├ ' ;
721+ $ rightT = '┤ ' ;
722+
723+ // Group items into rows
724+ $ rows = array_chunk ($ items , $ cellsPerRow );
725+
726+ // Draw top border
727+ $ topBorder = $ topLeft ;
728+ for ($ i = 0 ; $ i < $ cellsPerRow ; $ i ++) {
729+ $ topBorder .= str_repeat ($ horizontal , $ cellWidth );
730+ if ($ i < $ cellsPerRow - 1 ) {
731+ $ topBorder .= $ topT ;
732+ }
733+ }
734+ $ topBorder .= $ topRight ;
735+ $ this ->line ($ topBorder );
736+
737+ // Draw rows
738+ $ rowCount = 0 ;
739+ foreach ($ rows as $ row ) {
740+ $ line = $ vertical ;
741+ for ($ i = 0 ; $ i < $ cellsPerRow ; $ i ++) {
742+ if (isset ($ row [$ i ])) {
743+ $ item = $ row [$ i ];
744+ $ padding = $ cellWidth - mb_strlen ($ item ) - 2 ;
745+ $ line .= ' ' .$ item .str_repeat (' ' , $ padding + 1 ).$ vertical ;
746+ } else {
747+ // Empty cell
748+ $ line .= str_repeat (' ' , $ cellWidth ).$ vertical ;
749+ }
750+ }
751+ $ this ->line ($ line );
752+
753+ // Draw separator between rows (except after last row)
754+ if ($ rowCount < count ($ rows ) - 1 ) {
755+ $ separator = $ leftT ;
756+ for ($ i = 0 ; $ i < $ cellsPerRow ; $ i ++) {
757+ $ separator .= str_repeat ($ horizontal , $ cellWidth );
758+ if ($ i < $ cellsPerRow - 1 ) {
759+ $ separator .= $ cross ;
760+ }
761+ }
762+ $ separator .= $ rightT ;
763+ $ this ->line ($ separator );
764+ }
765+ $ rowCount ++;
766+ }
767+
768+ // Draw bottom border
769+ $ bottomBorder = $ bottomLeft ;
770+ for ($ i = 0 ; $ i < $ cellsPerRow ; $ i ++) {
771+ $ bottomBorder .= str_repeat ($ horizontal , $ cellWidth );
772+ if ($ i < $ cellsPerRow - 1 ) {
773+ $ bottomBorder .= $ bottomT ;
774+ }
775+ }
776+ $ bottomBorder .= $ bottomRight ;
777+ $ this ->line ($ bottomBorder );
778+ }
779+
588780 protected function enactMcpServers (): void
589781 {
590782 $ this ->newLine ();
591783 $ this ->info ('Installing MCP servers to your selected IDEs ' );
592784 $ this ->newLine ();
593785
594786 $ failed = [];
787+ $ longestIdeName = max (1 , ...$ this ->idesToInstallTo ->map (fn ($ ide ) => Str::length (class_basename ($ ide )))->toArray ());
595788
596789 foreach ($ this ->idesToInstallTo as $ ide ) {
597790 $ ideName = class_basename ($ ide );
598- $ this ->output ->write (" {$ ideName }... " );
791+ $ ideDisplay = str_pad ($ ideName , $ longestIdeName , ' ' , STR_PAD_RIGHT );
792+ $ this ->output ->write (" {$ ideDisplay }... " );
599793 $ results = [];
600794
601795 // Install Laravel Boost MCP if enabled
602796 if ($ this ->installingMcp ()) {
603797 try {
604798 $ result = $ ide ->installMcp ('laravel-boost ' , base_path ('artisan ' ), ['boost:mcp ' ]);
605-
799+
606800 if ($ result ) {
607- $ results [] = ' ✓ Boost ' ;
801+ $ results [] = $ this -> greenTick . ' Boost ' ;
608802 } else {
609- $ results [] = ' ✗ Boost ' ;
803+ $ results [] = $ this -> redCross . ' Boost ' ;
610804 $ failed [$ ideName ]['boost ' ] = 'Failed to write configuration ' ;
611805 }
612806 } catch (\Exception $ e ) {
613- $ results [] = ' ✗ Boost ' ;
807+ $ results [] = $ this -> redCross . ' Boost ' ;
614808 $ failed [$ ideName ]['boost ' ] = $ e ->getMessage ();
615809 }
616810 }
@@ -619,15 +813,15 @@ protected function enactMcpServers(): void
619813 if ($ this ->installingHerdMcp ()) {
620814 try {
621815 $ result = $ ide ->installMcp ('herd ' , PHP_BINARY , [$ this ->herdMcpPath ()]);
622-
816+
623817 if ($ result ) {
624- $ results [] = ' ✓ Herd ' ;
818+ $ results [] = $ this -> greenTick . ' Herd ' ;
625819 } else {
626- $ results [] = ' ✗ Herd ' ;
820+ $ results [] = $ this -> redCross . ' Herd ' ;
627821 $ failed [$ ideName ]['herd ' ] = 'Failed to write configuration ' ;
628822 }
629823 } catch (\Exception $ e ) {
630- $ results [] = ' ✗ Herd ' ;
824+ $ results [] = $ this -> redCross . ' Herd ' ;
631825 $ failed [$ ideName ]['herd ' ] = $ e ->getMessage ();
632826 }
633827 }
0 commit comments