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