github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/extension/command.go (about)

     1  package extension
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/cli/cli/git"
    11  	"github.com/cli/cli/internal/ghrepo"
    12  	"github.com/cli/cli/pkg/cmdutil"
    13  	"github.com/cli/cli/pkg/extensions"
    14  	"github.com/cli/cli/utils"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
    19  	m := f.ExtensionManager
    20  	io := f.IOStreams
    21  
    22  	extCmd := cobra.Command{
    23  		Use:   "extension",
    24  		Short: "Manage gh extensions",
    25  		Long: heredoc.Docf(`
    26  			GitHub CLI extensions are repositories that provide additional gh commands.
    27  
    28  			The name of the extension repository must start with "gh-" and it must contain an
    29  			executable of the same name. All arguments passed to the %[1]sgh <extname>%[1]s invocation
    30  			will be forwarded to the %[1]sgh-<extname>%[1]s executable of the extension.
    31  
    32  			An extension cannot override any of the core gh commands.
    33  		`, "`"),
    34  		Aliases: []string{"extensions"},
    35  	}
    36  
    37  	extCmd.AddCommand(
    38  		&cobra.Command{
    39  			Use:   "list",
    40  			Short: "List installed extension commands",
    41  			Args:  cobra.NoArgs,
    42  			RunE: func(cmd *cobra.Command, args []string) error {
    43  				cmds := m.List(true)
    44  				if len(cmds) == 0 {
    45  					return errors.New("no extensions installed")
    46  				}
    47  				cs := io.ColorScheme()
    48  				t := utils.NewTablePrinter(io)
    49  				for _, c := range cmds {
    50  					var repo string
    51  					if u, err := git.ParseURL(c.URL()); err == nil {
    52  						if r, err := ghrepo.FromURL(u); err == nil {
    53  							repo = ghrepo.FullName(r)
    54  						}
    55  					}
    56  
    57  					t.AddField(fmt.Sprintf("gh %s", c.Name()), nil, nil)
    58  					t.AddField(repo, nil, nil)
    59  					var updateAvailable string
    60  					if c.UpdateAvailable() {
    61  						updateAvailable = "Upgrade available"
    62  					}
    63  					t.AddField(updateAvailable, nil, cs.Green)
    64  					t.EndRow()
    65  				}
    66  				return t.Render()
    67  			},
    68  		},
    69  		&cobra.Command{
    70  			Use:   "install <repo>",
    71  			Short: "Install a gh extension from a repository",
    72  			Args:  cmdutil.MinimumArgs(1, "must specify a repository to install from"),
    73  			RunE: func(cmd *cobra.Command, args []string) error {
    74  				if args[0] == "." {
    75  					wd, err := os.Getwd()
    76  					if err != nil {
    77  						return err
    78  					}
    79  					return m.InstallLocal(wd)
    80  				}
    81  
    82  				repo, err := ghrepo.FromFullName(args[0])
    83  				if err != nil {
    84  					return err
    85  				}
    86  				if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
    87  					return err
    88  				}
    89  
    90  				cfg, err := f.Config()
    91  				if err != nil {
    92  					return err
    93  				}
    94  				protocol, _ := cfg.Get(repo.RepoHost(), "git_protocol")
    95  				return m.Install(ghrepo.FormatRemoteURL(repo, protocol), io.Out, io.ErrOut)
    96  			},
    97  		},
    98  		func() *cobra.Command {
    99  			var flagAll bool
   100  			var flagForce bool
   101  			cmd := &cobra.Command{
   102  				Use:   "upgrade {<name> | --all}",
   103  				Short: "Upgrade installed extensions",
   104  				Args: func(cmd *cobra.Command, args []string) error {
   105  					if len(args) == 0 && !flagAll {
   106  						return &cmdutil.FlagError{Err: errors.New("must specify an extension to upgrade")}
   107  					}
   108  					if len(args) > 0 && flagAll {
   109  						return &cmdutil.FlagError{Err: errors.New("cannot use `--all` with extension name")}
   110  					}
   111  					if len(args) > 1 {
   112  						return &cmdutil.FlagError{Err: errors.New("too many arguments")}
   113  					}
   114  					return nil
   115  				},
   116  				RunE: func(cmd *cobra.Command, args []string) error {
   117  					var name string
   118  					if len(args) > 0 {
   119  						name = normalizeExtensionSelector(args[0])
   120  					}
   121  					return m.Upgrade(name, flagForce, io.Out, io.ErrOut)
   122  				},
   123  			}
   124  			cmd.Flags().BoolVar(&flagAll, "all", false, "Upgrade all extensions")
   125  			cmd.Flags().BoolVar(&flagForce, "force", false, "Force upgrade extension")
   126  			return cmd
   127  		}(),
   128  		&cobra.Command{
   129  			Use:   "remove <name>",
   130  			Short: "Remove an installed extension",
   131  			Args:  cobra.ExactArgs(1),
   132  			RunE: func(cmd *cobra.Command, args []string) error {
   133  				extName := normalizeExtensionSelector(args[0])
   134  				if err := m.Remove(extName); err != nil {
   135  					return err
   136  				}
   137  				if io.IsStdoutTTY() {
   138  					cs := io.ColorScheme()
   139  					fmt.Fprintf(io.Out, "%s Removed extension %s\n", cs.SuccessIcon(), extName)
   140  				}
   141  				return nil
   142  			},
   143  		},
   144  		&cobra.Command{
   145  			Use:   "create <name>",
   146  			Short: "Create a new extension",
   147  			Args:  cmdutil.ExactArgs(1, "must specify a name for the extension"),
   148  			RunE: func(cmd *cobra.Command, args []string) error {
   149  				extName := args[0]
   150  				if !strings.HasPrefix(extName, "gh-") {
   151  					extName = "gh-" + extName
   152  				}
   153  				if err := m.Create(extName); err != nil {
   154  					return err
   155  				}
   156  				if !io.IsStdoutTTY() {
   157  					return nil
   158  				}
   159  				link := "https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions"
   160  				cs := io.ColorScheme()
   161  				out := heredoc.Docf(`
   162  					%[1]s Created directory %[2]s
   163  					%[1]s Initialized git repository
   164  					%[1]s Set up extension scaffolding
   165  
   166  					%[2]s is ready for development
   167  
   168  					Install locally with: cd %[2]s && gh extension install .
   169  
   170  					Publish to GitHub with: gh repo create %[2]s
   171  
   172  					For more information on writing extensions:
   173  					%[3]s
   174  				`, cs.SuccessIcon(), extName, link)
   175  				fmt.Fprint(io.Out, out)
   176  				return nil
   177  			},
   178  		},
   179  	)
   180  
   181  	return &extCmd
   182  }
   183  
   184  func checkValidExtension(rootCmd *cobra.Command, m extensions.ExtensionManager, extName string) error {
   185  	if !strings.HasPrefix(extName, "gh-") {
   186  		return errors.New("extension repository name must start with `gh-`")
   187  	}
   188  
   189  	commandName := strings.TrimPrefix(extName, "gh-")
   190  	if c, _, err := rootCmd.Traverse([]string{commandName}); err != nil {
   191  		return err
   192  	} else if c != rootCmd {
   193  		return fmt.Errorf("%q matches the name of a built-in command", commandName)
   194  	}
   195  
   196  	for _, ext := range m.List(false) {
   197  		if ext.Name() == commandName {
   198  			return fmt.Errorf("there is already an installed extension that provides the %q command", commandName)
   199  		}
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  func normalizeExtensionSelector(n string) string {
   206  	if idx := strings.IndexRune(n, '/'); idx >= 0 {
   207  		n = n[idx+1:]
   208  	}
   209  	return strings.TrimPrefix(n, "gh-")
   210  }