@@ -0,0 +1,108 @@
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+ 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+
56+func 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+}