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 }