| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- package cmd
- import (
- "cmp"
- "context"
- "fmt"
- "os"
- "os/signal"
- "charm.land/lipgloss/v2"
- "github.com/atotto/clipboard"
- hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
- "github.com/charmbracelet/crush/internal/config"
- "github.com/charmbracelet/crush/internal/oauth"
- "github.com/charmbracelet/crush/internal/oauth/copilot"
- "github.com/charmbracelet/crush/internal/oauth/hyper"
- "github.com/pkg/browser"
- "github.com/spf13/cobra"
- )
- var loginCmd = &cobra.Command{
- Aliases: []string{"auth"},
- Use: "login [platform]",
- Short: "Login Crush to a platform",
- Long: `Login Crush to a specified platform.
- The platform should be provided as an argument.
- Available platforms are: hyper, copilot.`,
- Example: `
- # Authenticate with Charm Hyper
- crush login
- # Authenticate with GitHub Copilot
- crush login copilot
- `,
- ValidArgs: []cobra.Completion{
- "hyper",
- "copilot",
- "github",
- "github-copilot",
- },
- Args: cobra.MaximumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- app, err := setupAppWithProgressBar(cmd)
- if err != nil {
- return err
- }
- defer app.Shutdown()
- provider := "hyper"
- if len(args) > 0 {
- provider = args[0]
- }
- switch provider {
- case "hyper":
- return loginHyper()
- case "copilot", "github", "github-copilot":
- return loginCopilot()
- default:
- return fmt.Errorf("unknown platform: %s", args[0])
- }
- },
- }
- func loginHyper() error {
- cfg := config.Get()
- if !hyperp.Enabled() {
- return fmt.Errorf("hyper not enabled")
- }
- ctx := getLoginContext()
- resp, err := hyper.InitiateDeviceAuth(ctx)
- if err != nil {
- return err
- }
- if clipboard.WriteAll(resp.UserCode) == nil {
- fmt.Println("The following code should be on clipboard already:")
- } else {
- fmt.Println("Copy the following code:")
- }
- fmt.Println()
- fmt.Println(lipgloss.NewStyle().Bold(true).Render(resp.UserCode))
- fmt.Println()
- fmt.Println("Press enter to open this URL, and then paste it there:")
- fmt.Println()
- fmt.Println(lipgloss.NewStyle().Hyperlink(resp.VerificationURL, "id=hyper").Render(resp.VerificationURL))
- fmt.Println()
- waitEnter()
- if err := browser.OpenURL(resp.VerificationURL); err != nil {
- fmt.Println("Could not open the URL. You'll need to manually open the URL in your browser.")
- }
- fmt.Println("Exchanging authorization code...")
- refreshToken, err := hyper.PollForToken(ctx, resp.DeviceCode, resp.ExpiresIn)
- if err != nil {
- return err
- }
- fmt.Println("Exchanging refresh token for access token...")
- token, err := hyper.ExchangeToken(ctx, refreshToken)
- if err != nil {
- return err
- }
- fmt.Println("Verifying access token...")
- introspect, err := hyper.IntrospectToken(ctx, token.AccessToken)
- if err != nil {
- return fmt.Errorf("token introspection failed: %w", err)
- }
- if !introspect.Active {
- return fmt.Errorf("access token is not active")
- }
- if err := cmp.Or(
- cfg.SetConfigField("providers.hyper.api_key", token.AccessToken),
- cfg.SetConfigField("providers.hyper.oauth", token),
- ); err != nil {
- return err
- }
- fmt.Println()
- fmt.Println("You're now authenticated with Hyper!")
- return nil
- }
- func loginCopilot() error {
- ctx := getLoginContext()
- cfg := config.Get()
- if cfg.HasConfigField("providers.copilot.oauth") {
- fmt.Println("You are already logged in to GitHub Copilot.")
- return nil
- }
- diskToken, hasDiskToken := copilot.RefreshTokenFromDisk()
- var token *oauth.Token
- switch {
- case hasDiskToken:
- fmt.Println("Found existing GitHub Copilot token on disk. Using it to authenticate...")
- t, err := copilot.RefreshToken(ctx, diskToken)
- if err != nil {
- return fmt.Errorf("unable to refresh token from disk: %w", err)
- }
- token = t
- default:
- fmt.Println("Requesting device code from GitHub...")
- dc, err := copilot.RequestDeviceCode(ctx)
- if err != nil {
- return err
- }
- fmt.Println()
- fmt.Println("Open the following URL and follow the instructions to authenticate with GitHub Copilot:")
- fmt.Println()
- fmt.Println(lipgloss.NewStyle().Hyperlink(dc.VerificationURI, "id=copilot").Render(dc.VerificationURI))
- fmt.Println()
- fmt.Println("Code:", lipgloss.NewStyle().Bold(true).Render(dc.UserCode))
- fmt.Println()
- fmt.Println("Waiting for authorization...")
- t, err := copilot.PollForToken(ctx, dc)
- if err == copilot.ErrNotAvailable {
- fmt.Println()
- fmt.Println("GitHub Copilot is unavailable for this account. To signup, go to the following page:")
- fmt.Println()
- fmt.Println(lipgloss.NewStyle().Hyperlink(copilot.SignupURL, "id=copilot-signup").Render(copilot.SignupURL))
- fmt.Println()
- fmt.Println("You may be able to request free access if eligible. For more information, see:")
- fmt.Println()
- fmt.Println(lipgloss.NewStyle().Hyperlink(copilot.FreeURL, "id=copilot-free").Render(copilot.FreeURL))
- }
- if err != nil {
- return err
- }
- token = t
- }
- if err := cmp.Or(
- cfg.SetConfigField("providers.copilot.api_key", token.AccessToken),
- cfg.SetConfigField("providers.copilot.oauth", token),
- ); err != nil {
- return err
- }
- fmt.Println()
- fmt.Println("You're now authenticated with GitHub Copilot!")
- return nil
- }
- func getLoginContext() context.Context {
- ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
- go func() {
- <-ctx.Done()
- cancel()
- os.Exit(1)
- }()
- return ctx
- }
- func waitEnter() {
- _, _ = fmt.Scanln()
- }
|