Skip to content

Commit 95ce49b

Browse files
authored
feat: migrate OAuth CLI flow to modular Bubble Tea TUI (#5)
* feat: migrate OAuth CLI flow to modular Bubble Tea TUI - Migrate all OAuth logic into a new TUI layer using Bubble Tea, decoupling it from the main CLI implementation - Relocate all token storage types, PKCE parameters, and related error definitions into tui/types.go - Implement a state-machine-driven OAuth2 TUI with clear step indicators for token checking, refreshing, auth flow, and API calls - Add visual styles and layout for terminal output, using lipgloss for enhanced readability and warnings - Replace direct console output with TUI workflow; warnings are now displayed in the interface, and user-visible prints in main.go are removed - Refactor code to pass operations such as token loading, saving, PKCE generation, and HTTP exchange through dependency injection (tui.Deps struct) - Remove all old-style direct I/O and logic from main.go and related files in favor of the TUI workflow - Update all code and test references to use the new tui types and interfaces - Add dependencies on Bubble Tea and related libraries for TUI support in go.mod Signed-off-by: appleboy <appleboy.tw@gmail.com> * refactor: refactor code formatting and improve readability throughout - Update mockExchangeFn in tests to accept parameters on separate lines for clarity - Fix string concatenation in config warning to avoid using fmt.Sprintf - Remove unnecessary blank lines from main.go - Move stop() call after TUI error in main to ensure correct cleanup - Improve formatting of program initialization for readability - Adjust return formatting in OAuthModel Update for better readability and consistency - Replace C-like for loop with Go-style range in OAuthModel View for clarity - Improve multiline code formatting to increase maintainability and consistency Signed-off-by: appleboy <appleboy.tw@gmail.com> * refactor: enhance token refresh handling and improve UI feedback - Refactor refresh token logic to return a warning if saving tokens fails, and surfacing the warning in the UI - Update dependencies interface to handle both a possible save warning and error when refreshing tokens - Improve URL wrapping to account for indented continuation lines for better readability Signed-off-by: appleboy <appleboy.tw@gmail.com> --------- Signed-off-by: appleboy <appleboy.tw@gmail.com>
1 parent fcb971a commit 95ce49b

13 files changed

Lines changed: 787 additions & 242 deletions

File tree

callback.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"net/http"
99
"sync"
1010
"time"
11+
12+
"github.com/go-authgate/oauth-cli/tui"
1113
)
1214

1315
const (
@@ -22,7 +24,7 @@ const (
2224

2325
// callbackResult holds the outcome of the local callback round-trip.
2426
type callbackResult struct {
25-
Storage *TokenStorage
27+
Storage *tui.TokenStorage
2628
Error string
2729
Desc string
2830
}
@@ -37,8 +39,8 @@ func startCallbackServer(
3739
ctx context.Context,
3840
port int,
3941
expectedState string,
40-
exchangeFn func(ctx context.Context, code string) (*TokenStorage, error),
41-
) (*TokenStorage, error) {
42+
exchangeFn func(ctx context.Context, code string) (*tui.TokenStorage, error),
43+
) (*tui.TokenStorage, error) {
4244
resultCh := make(chan callbackResult, 1)
4345

4446
// sendResult delivers the result exactly once. Any concurrent or subsequent
@@ -53,7 +55,7 @@ func startCallbackServer(
5355
// browser retries the callback request.
5456
var (
5557
exchangeOnce sync.Once
56-
exchangeStorage *TokenStorage
58+
exchangeStorage *tui.TokenStorage
5759
exchangeErr error
5860
)
5961

callback_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import (
99
"strings"
1010
"testing"
1111
"time"
12+
13+
"github.com/go-authgate/oauth-cli/tui"
1214
)
1315

1416
type serverResult struct {
15-
storage *TokenStorage
17+
storage *tui.TokenStorage
1618
err error
1719
}
1820

@@ -22,7 +24,7 @@ func startCallbackServerAsync(
2224
t *testing.T,
2325
port int,
2426
state string,
25-
exchangeFn func(ctx context.Context, code string) (*TokenStorage, error),
27+
exchangeFn func(ctx context.Context, code string) (*tui.TokenStorage, error),
2628
) chan serverResult {
2729
t.Helper()
2830
ch := make(chan serverResult, 1)
@@ -36,10 +38,12 @@ func startCallbackServerAsync(
3638
}
3739

3840
// mockExchangeFn returns an exchangeFn that succeeds with a stub TokenStorage.
39-
func mockExchangeFn(t *testing.T) func(ctx context.Context, code string) (*TokenStorage, error) {
41+
func mockExchangeFn(
42+
t *testing.T,
43+
) func(ctx context.Context, code string) (*tui.TokenStorage, error) {
4044
t.Helper()
41-
return func(_ context.Context, _ string) (*TokenStorage, error) {
42-
return &TokenStorage{
45+
return func(_ context.Context, _ string) (*tui.TokenStorage, error) {
46+
return &tui.TokenStorage{
4347
AccessToken: "mock-access-token",
4448
RefreshToken: "mock-refresh-token",
4549
TokenType: "Bearer",
@@ -91,7 +95,7 @@ func TestCallbackServer_ExchangeFailure(t *testing.T) {
9195
const port = 19006
9296
state := "test-state-exchange-fail"
9397

94-
failFn := func(_ context.Context, _ string) (*TokenStorage, error) {
98+
failFn := func(_ context.Context, _ string) (*tui.TokenStorage, error) {
9599
return nil, errors.New("server returned status 400: invalid_grant")
96100
}
97101
ch := startCallbackServerAsync(t, port, state, failFn)

go.mod

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
module github.com/go-authgate/oauth-cli
22

3-
go 1.24
3+
go 1.24.2
44

55
require (
6+
charm.land/bubbles/v2 v2.0.0
7+
charm.land/bubbletea/v2 v2.0.0
8+
charm.land/lipgloss/v2 v2.0.0
69
github.com/appleboy/go-httpretry v0.11.0
710
github.com/google/uuid v1.6.0
811
github.com/joho/godotenv v1.5.1
912
)
13+
14+
require (
15+
github.com/charmbracelet/colorprofile v0.4.2 // indirect
16+
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
17+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
18+
github.com/charmbracelet/x/term v0.2.2 // indirect
19+
github.com/charmbracelet/x/termios v0.1.1 // indirect
20+
github.com/charmbracelet/x/windows v0.2.2 // indirect
21+
github.com/clipperhouse/displaywidth v0.11.0 // indirect
22+
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
23+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
24+
github.com/mattn/go-runewidth v0.0.20 // indirect
25+
github.com/muesli/cancelreader v0.2.2 // indirect
26+
github.com/rivo/uniseg v0.4.7 // indirect
27+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
28+
golang.org/x/sync v0.19.0 // indirect
29+
golang.org/x/sys v0.41.0 // indirect
30+
)

go.sum

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
1+
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
2+
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
3+
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
4+
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
5+
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
6+
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
17
github.com/appleboy/go-httpretry v0.11.0 h1:LI2kFDBI9ghxIip9dJz3uRMEVEwSSOC1bjS177QCi+w=
28
github.com/appleboy/go-httpretry v0.11.0/go.mod h1:96v1IO6wg1+S10iFbOM3O8rn2vkFw8+uH4mDPhGoz+E=
9+
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
10+
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
11+
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
12+
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
13+
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
14+
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
15+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
16+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
17+
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
18+
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
19+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
20+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
21+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
22+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
23+
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
24+
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
25+
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
26+
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
27+
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
28+
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
329
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
430
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
531
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
632
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
33+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
34+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
35+
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
36+
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
37+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
38+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
39+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
40+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
41+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
42+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
43+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
44+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
45+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
46+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
47+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
48+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

0 commit comments

Comments
 (0)