oauth_copilot.go

 1package dialog
 2
 3import (
 4	"context"
 5	"fmt"
 6	"time"
 7
 8	tea "charm.land/bubbletea/v2"
 9	"charm.land/catwalk/pkg/catwalk"
10	"github.com/charmbracelet/crush/internal/config"
11	"github.com/charmbracelet/crush/internal/oauth/copilot"
12	"github.com/charmbracelet/crush/internal/ui/common"
13)
14
15func NewOAuthCopilot(
16	com *common.Common,
17	isOnboarding bool,
18	provider catwalk.Provider,
19	model config.SelectedModel,
20	modelType config.SelectedModelType,
21) (*OAuth, tea.Cmd) {
22	return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthCopilot{})
23}
24
25type OAuthCopilot struct {
26	deviceCode *copilot.DeviceCode
27	cancelFunc func()
28}
29
30var _ OAuthProvider = (*OAuthCopilot)(nil)
31
32func (m *OAuthCopilot) name() string {
33	return "GitHub Copilot"
34}
35
36func (m *OAuthCopilot) initiateAuth() tea.Msg {
37	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
38	defer cancel()
39
40	deviceCode, err := copilot.RequestDeviceCode(ctx)
41	if err != nil {
42		return ActionOAuthErrored{Error: fmt.Errorf("failed to initiate device auth: %w", err)}
43	}
44
45	m.deviceCode = deviceCode
46
47	return ActionInitiateOAuth{
48		DeviceCode:      deviceCode.DeviceCode,
49		UserCode:        deviceCode.UserCode,
50		VerificationURL: deviceCode.VerificationURI,
51		ExpiresIn:       deviceCode.ExpiresIn,
52		Interval:        deviceCode.Interval,
53	}
54}
55
56func (m *OAuthCopilot) startPolling(deviceCode string, expiresIn int) tea.Cmd {
57	return func() tea.Msg {
58		ctx, cancel := context.WithCancel(context.Background())
59		m.cancelFunc = cancel
60
61		token, err := copilot.PollForToken(ctx, m.deviceCode)
62		if err != nil {
63			if ctx.Err() != nil {
64				return nil // cancelled, don't report error.
65			}
66			return ActionOAuthErrored{Error: err}
67		}
68
69		return ActionCompleteOAuth{Token: token}
70	}
71}
72
73func (m *OAuthCopilot) stopPolling() tea.Msg {
74	if m.cancelFunc != nil {
75		m.cancelFunc()
76	}
77	return nil
78}