login.go

  1package cmd
  2
  3import (
  4	"cmp"
  5	"context"
  6	"fmt"
  7	"os"
  8	"os/signal"
  9	"strings"
 10
 11	"charm.land/lipgloss/v2"
 12	"github.com/charmbracelet/crush/internal/config"
 13	"github.com/charmbracelet/crush/internal/oauth/claude"
 14	"github.com/spf13/cobra"
 15)
 16
 17var loginCmd = &cobra.Command{
 18	Aliases: []string{"auth"},
 19	Use:     "login [platform]",
 20	Short:   "Login Crush to a platform",
 21	Long: `Login Crush to a specified platform.
 22The platform should be provided as an argument.
 23Available platforms are: claude.`,
 24	Example: `
 25# Authenticate with Claude Code Max
 26crush login claude
 27  `,
 28	ValidArgs: []cobra.Completion{
 29		"claude",
 30		"anthropic",
 31	},
 32	Args: cobra.ExactArgs(1),
 33	RunE: func(cmd *cobra.Command, args []string) error {
 34		if len(args) > 1 {
 35			return fmt.Errorf("wrong number of arguments")
 36		}
 37		if len(args) == 0 || args[0] == "" {
 38			return cmd.Help()
 39		}
 40
 41		app, err := setupAppWithProgressBar(cmd)
 42		if err != nil {
 43			return err
 44		}
 45		defer app.Shutdown()
 46
 47		switch args[0] {
 48		case "anthropic", "claude":
 49			return loginClaude()
 50		default:
 51			return fmt.Errorf("unknown platform: %s", args[0])
 52		}
 53	},
 54}
 55
 56func loginClaude() error {
 57	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
 58	go func() {
 59		<-ctx.Done()
 60		cancel()
 61		os.Exit(1)
 62	}()
 63
 64	verifier, challenge, err := claude.GetChallenge()
 65	if err != nil {
 66		return err
 67	}
 68	url, err := claude.AuthorizeURL(verifier, challenge)
 69	if err != nil {
 70		return err
 71	}
 72	fmt.Println("Open the following URL and follow the instructions to authenticate with Claude Code Max:")
 73	fmt.Println()
 74	fmt.Println(lipgloss.NewStyle().Hyperlink(url, "id=claude").Render(url))
 75	fmt.Println()
 76	fmt.Println("Press enter to continue...")
 77	if _, err := fmt.Scanln(); err != nil {
 78		return err
 79	}
 80
 81	fmt.Println("Now paste and code from Anthropic and press enter...")
 82	fmt.Println()
 83	fmt.Print("> ")
 84	var code string
 85	for code == "" {
 86		_, _ = fmt.Scanln(&code)
 87		code = strings.TrimSpace(code)
 88	}
 89
 90	fmt.Println()
 91	fmt.Println("Exchanging authorization code...")
 92	token, err := claude.ExchangeToken(ctx, code, verifier)
 93	if err != nil {
 94		return err
 95	}
 96
 97	cfg := config.Get()
 98	if err := cmp.Or(
 99		cfg.SetConfigField("providers.anthropic.api_key", token.AccessToken),
100		cfg.SetConfigField("providers.anthropic.oauth", token),
101	); err != nil {
102		return err
103	}
104
105	fmt.Println()
106	fmt.Println("You're now authenticated with Claude Code Max!")
107	return nil
108}