feat: add `crush login claude` command (#1537)

Andrey Nering created

Change summary

internal/cmd/login.go | 103 +++++++++++++++++++++++++++++++++++++++++++++
internal/cmd/root.go  |   1 
2 files changed, 104 insertions(+)

Detailed changes

internal/cmd/login.go 🔗

@@ -0,0 +1,103 @@
+package cmd
+
+import (
+	"cmp"
+	"context"
+	"fmt"
+	"os"
+	"os/signal"
+	"strings"
+
+	"charm.land/lipgloss/v2"
+	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/oauth/claude"
+	"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: claude.`,
+	Example: `
+# Authenticate with Claude Code Max
+crush login claude
+  `,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) > 1 {
+			return fmt.Errorf("wrong number of arguments")
+		}
+		if len(args) == 0 || args[0] == "" {
+			return cmd.Help()
+		}
+
+		app, err := setupAppWithProgressBar(cmd)
+		if err != nil {
+			return err
+		}
+		defer app.Shutdown()
+
+		switch args[0] {
+		case "anthropic", "claude":
+			return loginClaude()
+		default:
+			return fmt.Errorf("unknown platform: %s", args[0])
+		}
+	},
+}
+
+func loginClaude() error {
+	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
+	go func() {
+		<-ctx.Done()
+		cancel()
+		os.Exit(1)
+	}()
+
+	verifier, challenge, err := claude.GetChallenge()
+	if err != nil {
+		return err
+	}
+	url, err := claude.AuthorizeURL(verifier, challenge)
+	if err != nil {
+		return err
+	}
+	fmt.Println("Open the following URL and follow the instructions to authenticate with Claude Code Max:")
+	fmt.Println()
+	fmt.Println(lipgloss.NewStyle().Hyperlink(url, "id=claude").Render(url))
+	fmt.Println()
+	fmt.Println("Press enter to continue...")
+	if _, err := fmt.Scanln(); err != nil {
+		return err
+	}
+
+	fmt.Println("Now paste and code from Anthropic and press enter...")
+	fmt.Println()
+	fmt.Print("> ")
+	var code string
+	for code == "" {
+		_, _ = fmt.Scanln(&code)
+		code = strings.TrimSpace(code)
+	}
+
+	fmt.Println()
+	fmt.Println("Exchanging authorization code...")
+	token, err := claude.ExchangeToken(ctx, code, verifier)
+	if err != nil {
+		return err
+	}
+
+	cfg := config.Get()
+	if err := cmp.Or(
+		cfg.SetConfigField("providers.anthropic.api_key", token.AccessToken),
+		cfg.SetConfigField("providers.anthropic.oauth", token),
+	); err != nil {
+		return err
+	}
+
+	fmt.Println()
+	fmt.Println("You're now authenticated with Claude Code Max!")
+	return nil
+}