github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/cmd/sm/main.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "net" 8 "os" 9 "os/exec" 10 "path" 11 "path/filepath" 12 "strings" 13 "time" 14 15 surveyCore "github.com/AlecAivazis/survey/v2/core" 16 "github.com/AlecAivazis/survey/v2/terminal" 17 "github.com/abdfnx/gh-api/api" 18 "github.com/abdfnx/gh-api/internal/build" 19 "github.com/abdfnx/gh-api/internal/config" 20 "github.com/abdfnx/gh-api/internal/ghinstance" 21 "github.com/abdfnx/gh-api/internal/ghrepo" 22 "github.com/abdfnx/gh-api/internal/run" 23 "github.com/abdfnx/gh-api/internal/update" 24 "github.com/abdfnx/gh-api/pkg/cmd/alias/expand" 25 "github.com/abdfnx/gh-api/pkg/cmd/factory" 26 "github.com/abdfnx/gh-api/pkg/cmd/root" 27 "github.com/abdfnx/gh-api/pkg/cmdutil" 28 "github.com/abdfnx/gh-api/utils" 29 "github.com/cli/safeexec" 30 "github.com/mattn/go-colorable" 31 "github.com/mgutz/ansi" 32 "github.com/spf13/cobra" 33 ) 34 35 var updaterEnabled = "" 36 37 type exitCode int 38 39 const ( 40 exitOK exitCode = 0 41 exitError exitCode = 1 42 exitCancel exitCode = 2 43 exitAuth exitCode = 4 44 ) 45 46 func main() { 47 code := mainRun() 48 os.Exit(int(code)) 49 } 50 51 func mainRun() exitCode { 52 buildDate := build.Date 53 buildVersion := build.Version 54 55 updateMessageChan := make(chan *update.ReleaseInfo) 56 go func() { 57 rel, _ := checkForUpdate(buildVersion) 58 updateMessageChan <- rel 59 }() 60 61 hasDebug := os.Getenv("DEBUG") != "" 62 63 cmdFactory := factory.New(buildVersion) 64 stderr := cmdFactory.IOStreams.ErrOut 65 if !cmdFactory.IOStreams.ColorEnabled() { 66 surveyCore.DisableColor = true 67 } else { 68 // override survey's poor choice of color 69 surveyCore.TemplateFuncsWithColor["color"] = func(style string) string { 70 switch style { 71 case "white": 72 if cmdFactory.IOStreams.ColorSupport256() { 73 return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242) 74 } 75 return ansi.ColorCode("default") 76 default: 77 return ansi.ColorCode(style) 78 } 79 } 80 } 81 82 // terminal. With this, a user can clone a repo (or take other actions) directly from explorer. 83 if len(os.Args) > 1 && os.Args[1] != "" { 84 cobra.MousetrapHelpText = "" 85 } 86 87 rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate) 88 89 cfg, err := cmdFactory.Config() 90 if err != nil { 91 fmt.Fprintf(stderr, "failed to read configuration: %s\n", err) 92 return exitError 93 } 94 95 if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" { 96 cmdFactory.IOStreams.SetNeverPrompt(true) 97 } 98 99 if pager, _ := cfg.Get("", "pager"); pager != "" { 100 cmdFactory.IOStreams.SetPager(pager) 101 } 102 103 // TODO: remove after FromFullName has been revisited 104 if host, err := cfg.DefaultHost(); err == nil { 105 ghrepo.SetDefaultHost(host) 106 } 107 108 expandedArgs := []string{} 109 if len(os.Args) > 0 { 110 expandedArgs = os.Args[1:] 111 } 112 113 cmd, _, err := rootCmd.Traverse(expandedArgs) 114 if err != nil || cmd == rootCmd { 115 originalArgs := expandedArgs 116 isShell := false 117 118 expandedArgs, isShell, err = expand.ExpandAlias(cfg, os.Args, nil) 119 if err != nil { 120 fmt.Fprintf(stderr, "failed to process aliases: %s\n", err) 121 return exitError 122 } 123 124 if hasDebug { 125 fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs) 126 } 127 128 if isShell { 129 exe, err := safeexec.LookPath(expandedArgs[0]) 130 if err != nil { 131 fmt.Fprintf(stderr, "failed to run external command: %s", err) 132 return exitError 133 } 134 135 externalCmd := exec.Command(exe, expandedArgs[1:]...) 136 externalCmd.Stderr = os.Stderr 137 externalCmd.Stdout = os.Stdout 138 externalCmd.Stdin = os.Stdin 139 preparedCmd := run.PrepareCmd(externalCmd) 140 141 err = preparedCmd.Run() 142 if err != nil { 143 if ee, ok := err.(*exec.ExitError); ok { 144 return exitCode(ee.ExitCode()) 145 } 146 147 fmt.Fprintf(stderr, "failed to run external command: %s", err) 148 return exitError 149 } 150 151 return exitOK 152 } 153 } 154 155 cs := cmdFactory.IOStreams.ColorScheme() 156 157 if cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) { 158 fmt.Fprintln(stderr, cs.Bold("Welcome to Secman Login!")) 159 fmt.Fprintln(stderr) 160 fmt.Fprintln(stderr, "To authenticate, please run `secman auth login`.") 161 return exitAuth 162 } 163 164 rootCmd.SetArgs(expandedArgs) 165 166 if cmd, err := rootCmd.ExecuteC(); err != nil { 167 if err == cmdutil.SilentError { 168 return exitError 169 } else if cmdutil.IsUserCancellation(err) { 170 if errors.Is(err, terminal.InterruptErr) { 171 // ensure the next shell prompt will start on its own line 172 fmt.Fprint(stderr, "\n") 173 } 174 return exitCancel 175 } 176 177 printError(stderr, err, cmd, hasDebug) 178 179 var httpErr api.HTTPError 180 if errors.As(err, &httpErr) && httpErr.StatusCode == 401 { 181 fmt.Fprintln(stderr, "hint: try authenticating with `secman auth login`") 182 } 183 184 return exitError 185 } 186 if root.HasFailed() { 187 return exitError 188 } 189 190 return exitOK 191 } 192 193 func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { 194 var dnsError *net.DNSError 195 if errors.As(err, &dnsError) { 196 fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name) 197 if debug { 198 fmt.Fprintln(out, dnsError) 199 } 200 fmt.Fprintln(out, "check your internet connection or githubstatus.com") 201 return 202 } 203 204 fmt.Fprintln(out, err) 205 206 var flagError *cmdutil.FlagError 207 if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") { 208 if !strings.HasSuffix(err.Error(), "\n") { 209 fmt.Fprintln(out) 210 } 211 fmt.Fprintln(out, cmd.UsageString()) 212 } 213 } 214 215 func shouldCheckForUpdate() bool { 216 if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { 217 return false 218 } 219 if os.Getenv("CODESPACES") != "" { 220 return false 221 } 222 return updaterEnabled != "" && !isCI() && utils.IsTerminal(os.Stdout) && utils.IsTerminal(os.Stderr) 223 } 224 225 // based on https://github.com/watson/ci-info/blob/HEAD/index.js 226 func isCI() bool { 227 return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari 228 os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity 229 os.Getenv("RUN_ID") != "" // TaskCluster, dsari 230 } 231 232 func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) { 233 if !shouldCheckForUpdate() { 234 return nil, nil 235 } 236 237 client, err := basicClient(currentVersion) 238 if err != nil { 239 return nil, err 240 } 241 242 repo := updaterEnabled 243 stateFilePath := path.Join(config.ConfigDir(), "state.yml") 244 return update.CheckForUpdate(client, stateFilePath, repo, currentVersion) 245 } 246 247 // BasicClient returns an API client for github.com only that borrows from but 248 // does not depend on user configuration 249 func basicClient(currentVersion string) (*api.Client, error) { 250 var opts []api.ClientOption 251 if verbose := os.Getenv("DEBUG"); verbose != "" { 252 opts = append(opts, apiVerboseLog()) 253 } 254 opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", currentVersion))) 255 256 token, _ := config.AuthTokenFromEnv(ghinstance.Default()) 257 if token == "" { 258 if c, err := config.ParseDefaultConfig(); err == nil { 259 token, _ = c.Get(ghinstance.Default(), "oauth_token") 260 } 261 } 262 if token != "" { 263 opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token))) 264 } 265 return api.NewClient(opts...), nil 266 } 267 268 func apiVerboseLog() api.ClientOption { 269 logTraffic := strings.Contains(os.Getenv("DEBUG"), "api") 270 colorize := utils.IsTerminal(os.Stderr) 271 return api.VerboseLog(colorable.NewColorable(os.Stderr), logTraffic, colorize) 272 } 273 274 func isRecentRelease(publishedAt time.Time) bool { 275 return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24 276 } 277 278 // Check whether the gh binary was found under the Homebrew prefix 279 func isUnderHomebrew(ghBinary string) bool { 280 brewExe, err := safeexec.LookPath("brew") 281 if err != nil { 282 return false 283 } 284 285 brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output() 286 if err != nil { 287 return false 288 } 289 290 brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator) 291 return strings.HasPrefix(ghBinary, brewBinPrefix) 292 }