package authswitch import ( "errors" "fmt" "slices" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/pkg/cmd/auth/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) type SwitchOptions struct { IO *iostreams.IOStreams Config func() (config.Config, error) Prompter shared.Prompt Hostname string Username string } func NewCmdSwitch(f *cmdutil.Factory, runF func(*SwitchOptions) error) *cobra.Command { opts := SwitchOptions{ IO: f.IOStreams, Config: f.Config, Prompter: f.Prompter, } cmd := &cobra.Command{ Use: "switch", Args: cobra.ExactArgs(0), Short: "Switch active GitHub account", Long: heredoc.Doc(` Switch the active account for a GitHub host. This command changes the authentication configuration that will be used when running commands targeting the specified GitHub host. `), Example: heredoc.Doc(` # Select what host and account to switch to via a prompt $ gh auth switch # Switch to a specific host and specific account $ gh auth logout --hostname enterprise.internal --user monalisa `), RunE: func(c *cobra.Command, args []string) error { if runF != nil { return runF(&opts) } return switchRun(&opts) }, } cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to switch account for") cmd.Flags().StringVarP(&opts.Username, "user", "u", "", "The account to switch to") return cmd } type hostUser struct { host string user string active bool } type candidates []hostUser func switchRun(opts *SwitchOptions) error { hostname := opts.Hostname username := opts.Username cfg, err := opts.Config() if err != nil { return err } authCfg := cfg.Authentication() knownHosts := authCfg.Hosts() if len(knownHosts) == 0 { return fmt.Errorf("not logged in to any hosts") } if hostname != "" { if !slices.Contains(knownHosts, hostname) { return fmt.Errorf("not logged in to %s", hostname) } if username != "" { knownUsers := cfg.Authentication().UsersForHost(hostname) if !slices.Contains(knownUsers, username) { return fmt.Errorf("not logged in to %s account %s", hostname, username) } } } var candidates candidates for _, host := range knownHosts { if hostname != "" && host != hostname { continue } hostActiveUser, err := authCfg.ActiveUser(host) if err != nil { return err } knownUsers := cfg.Authentication().UsersForHost(host) for _, user := range knownUsers { if username != "" && user != username { continue } candidates = append(candidates, hostUser{host: host, user: user, active: user == hostActiveUser}) } } if len(candidates) == 0 { return errors.New("no accounts matched that criteria") } else if len(candidates) == 1 { hostname = candidates[0].host username = candidates[0].user } else if len(candidates) == 2 && candidates[0].host == candidates[1].host { // If there is a single host with two users, automatically swith to the // inactive user without prompting. hostname = candidates[0].host username = candidates[0].user if candidates[0].active { username = candidates[1].user } } else if !opts.IO.CanPrompt() { return errors.New("unable to determine which account to switch to, please specify `--hostname` and `--user`") } else { prompts := make([]string, len(candidates)) for i, c := range candidates { prompt := fmt.Sprintf("%s (%s)", c.user, c.host) if c.active { prompt += " - active" } prompts[i] = prompt } selected, err := opts.Prompter.Select( "What account do you want to switch to?", "", prompts) if err != nil { return fmt.Errorf("could not prompt: %w", err) } hostname = candidates[selected].host username = candidates[selected].user } if src, writeable := shared.AuthTokenWriteable(authCfg, hostname); !writeable { fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", src) fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI manage credentials instead, first clear the value from the environment.\n") return cmdutil.SilentError } cs := opts.IO.ColorScheme() if err := authCfg.SwitchUser(hostname, username); err != nil { fmt.Fprintf(opts.IO.ErrOut, "%s Failed to switch account for %s to %s\n", cs.FailureIcon(), hostname, cs.Bold(username)) return err } fmt.Fprintf(opts.IO.ErrOut, "%s Switched active account for %s to %s\n", cs.SuccessIcon(), hostname, cs.Bold(username)) return nil }