--- internal/cmd/login.go +++ internal/cmd/login.go @@ -6,12 +6,14 @@ "fmt" "os" "os/signal" + "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/crush/internal/client" "github.com/charmbracelet/crush/internal/clipboard" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/oauth" + "github.com/charmbracelet/crush/internal/oauth/claude" "github.com/charmbracelet/crush/internal/oauth/copilot" "github.com/charmbracelet/crush/internal/oauth/hyper" "github.com/charmbracelet/x/ansi" @@ -25,11 +27,14 @@ 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.`, +Available platforms are: hyper, claude, copilot.`, Example: ` # Authenticate with Charm Hyper crush login +# Authenticate with Claude Code Max +crush login claude + # Authenticate with GitHub Copilot crush login copilot @@ -38,6 +43,8 @@ `, ValidArgs: []cobra.Completion{ "hyper", + "claude", + "anthropic", "copilot", "github", "github-copilot", @@ -64,6 +71,8 @@ switch provider { case "hyper": return loginHyper(c, ws.ID, force) + case "anthropic", "claude": + return loginClaude(c, ws.ID, force) case "copilot", "github", "github-copilot": return loginCopilot(c, ws.ID, force) default: @@ -140,6 +149,68 @@ fmt.Println() fmt.Println("You're now authenticated with Hyper!") + return nil +} + +func loginClaude(c *client.Client, wsID string, force bool) error { + ctx := getLoginContext() + + if !force { + cfg, err := c.GetConfig(ctx, wsID) + if err == nil && cfg != nil { + if pc, ok := cfg.Providers.Get("anthropic"); ok && pc.OAuthToken != nil { + fmt.Println("You are already logged in to Claude Code Max.") + fmt.Println("Use --force to re-authenticate.") + return nil + } + } + } + + verifier, challenge, err := claude.GetChallenge() + if err != nil { + return err + } + authURL, err := claude.AuthorizeURL(verifier, challenge) + if err != nil { + return err + } + + fmt.Println("Open the following URL to authorize your Claude Code Max subscription:") + fmt.Println() + fmt.Println(lipgloss.NewStyle().Hyperlink(authURL, "id=claude").Render(authURL)) + fmt.Println() + fmt.Println("Press enter to open in your browser...") + waitEnter() + if err := browser.OpenURL(authURL); err != nil { + fmt.Println("Could not open the URL. Open it manually in your browser.") + } + + fmt.Println() + fmt.Println("After authorizing, paste the code shown on the page and press enter:") + fmt.Println() + fmt.Print("> ") + var code string + for code == "" { + _, _ = fmt.Scan(&code) + code = strings.TrimSpace(code) + } + + fmt.Println() + fmt.Println("Exchanging authorization code...") + token, err := claude.ExchangeToken(ctx, code, verifier) + if err != nil { + return err + } + + if err := cmp.Or( + c.SetConfigField(ctx, wsID, config.ScopeGlobal, "providers.anthropic.api_key", token.AccessToken), + c.SetConfigField(ctx, wsID, config.ScopeGlobal, "providers.anthropic.oauth", token), + ); err != nil { + return err + } + + fmt.Println() + fmt.Println("You're now authenticated with Claude Code Max!") return nil }