121 lines
3.2 KiB
Plaintext
121 lines
3.2 KiB
Plaintext
--- 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
|
|
}
|
|
|