@@ -24,6 +24,7 @@ import (
2424 "github.com/cli/cli/v2/internal/skills/installer"
2525 "github.com/cli/cli/v2/internal/skills/registry"
2626 "github.com/cli/cli/v2/internal/skills/source"
27+ "github.com/cli/cli/v2/internal/tableprinter"
2728 "github.com/cli/cli/v2/internal/text"
2829 "github.com/cli/cli/v2/pkg/cmdutil"
2930 "github.com/cli/cli/v2/pkg/iostreams"
@@ -138,9 +139,14 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
138139 frontmatter. This metadata identifies the source repository and
139140 enables %[1]sgh skill update%[1]s to detect changes.
140141
142+ When run interactively, the command prompts for any missing arguments.
143+
141144 Use %[1]s--all%[1]s to install every discovered skill from the repository
142- without prompting for skill selection. When run non-interactively, %[1]srepository%[1]s and either
143- a skill name or %[1]s--all%[1]s are required.
145+ without prompting for skill selection. When run non-interactively,
146+ %[1]srepository%[1]s is required; without a skill name or %[1]s--all%[1]s the
147+ matching skills are listed (as tab-separated values when piped) so you can
148+ browse or filter them with tools like %[1]sgrep%[1]s before re-running with
149+ a specific skill.
144150 ` , "`" , registry .AgentHelpList ()),
145151 Example : heredoc .Doc (`
146152 # Interactive: choose repo, skill, and agent
@@ -149,6 +155,9 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
149155 # Choose a skill from the repo interactively
150156 $ gh skill install github/awesome-copilot
151157
158+ # List available skills non-interactively (e.g. to pipe into grep)
159+ $ gh skill install github/awesome-copilot | grep review
160+
152161 # Install a specific skill
153162 $ gh skill install github/awesome-copilot git-commit
154163
@@ -314,6 +323,9 @@ func installRun(opts *InstallOptions) error {
314323 },
315324 })
316325 if err != nil {
326+ if errors .Is (err , errSkillsListed ) {
327+ return nil
328+ }
317329 return err
318330 }
319331 }
@@ -507,6 +519,9 @@ func runLocalInstall(opts *InstallOptions) error {
507519 sourceHint : absSource ,
508520 })
509521 if err != nil {
522+ if errors .Is (err , errSkillsListed ) {
523+ return nil
524+ }
510525 return err
511526 }
512527
@@ -669,6 +684,12 @@ type installPlan struct {
669684 skills []discovery.Skill
670685}
671686
687+ // errSkillsListed is a sentinel returned by selectSkillsWithSelector when
688+ // the command runs non-interactively without a skill name. In that case the
689+ // selector prints the available skills to stdout (so they can be piped into
690+ // grep or similar) and the caller exits without installing.
691+ var errSkillsListed = errors .New ("skills listed" )
692+
672693func selectSkillsWithSelector (opts * InstallOptions , skills []discovery.Skill , canPrompt bool , sel skillSelector ) ([]discovery.Skill , error ) {
673694 checkCollisions := func (ss []discovery.Skill ) error {
674695 if err := collisionError (ss ); err != nil {
@@ -690,7 +711,10 @@ func selectSkillsWithSelector(opts *InstallOptions, skills []discovery.Skill, ca
690711 }
691712
692713 if ! canPrompt {
693- return nil , cmdutil .FlagErrorf ("must specify a skill name when not running interactively" )
714+ if err := listAvailableSkills (opts , skills , sel ); err != nil {
715+ return nil , err
716+ }
717+ return nil , errSkillsListed
694718 }
695719
696720 if sel .fetchDescriptions != nil {
@@ -734,6 +758,43 @@ func selectSkillsWithSelector(opts *InstallOptions, skills []discovery.Skill, ca
734758 return result , checkCollisions (result )
735759}
736760
761+ // listAvailableSkills prints discovered skills as a table for non-interactive
762+ // callers, mirroring the information shown in the interactive picker so the
763+ // output can be browsed or piped into tools like grep.
764+ func listAvailableSkills (opts * InstallOptions , skills []discovery.Skill , sel skillSelector ) error {
765+ if len (skills ) == 0 {
766+ return fmt .Errorf ("no skills found in %s" , sel .sourceHint )
767+ }
768+
769+ if sel .fetchDescriptions != nil {
770+ sel .fetchDescriptions ()
771+ }
772+
773+ if opts .IO .IsStdoutTTY () {
774+ fmt .Fprintf (opts .IO .ErrOut , "Showing %s from %s. Re-run with a skill name to install.\n \n " ,
775+ text .Pluralize (len (skills ), "skill" ), sel .sourceHint )
776+ }
777+
778+ tw := opts .IO .TerminalWidth ()
779+ descWidth := tw - 40
780+ if descWidth < 20 {
781+ descWidth = 20
782+ }
783+ isTTY := opts .IO .IsStdoutTTY ()
784+
785+ table := tableprinter .New (opts .IO , tableprinter .WithHeader ("SKILL" , "DESCRIPTION" ))
786+ for _ , s := range skills {
787+ table .AddField (s .DisplayName ())
788+ desc := s .Description
789+ if isTTY {
790+ desc = text .Truncate (descWidth , desc )
791+ }
792+ table .AddField (desc )
793+ table .EndRow ()
794+ }
795+ return table .Render ()
796+ }
797+
737798func matchSkillByName (opts * InstallOptions , skills []discovery.Skill ) ([]discovery.Skill , error ) {
738799 for _ , s := range skills {
739800 if s .DisplayName () == opts .SkillName {
0 commit comments