Skip to content

Add styled CLI output with spinner for logout command#37

Open
gtsiolis wants to merge 2 commits intomainfrom
george/des-113
Open

Add styled CLI output with spinner for logout command#37
gtsiolis wants to merge 2 commits intomainfrom
george/des-113

Conversation

@gtsiolis
Copy link
Member

@gtsiolis gtsiolis commented Feb 16, 2026

This will add styled output events and spinner to output package, using also

This will ...

  1. Add SuccessEvent and NoteEvent to the Sink system for consistent styled output
  2. Add reusable RunWithSpinner helper for operations with progress indicator
  3. Add styles.go with color definitions, adaptive for light and dark themes
  4. Update logout command to show spinner while running and styled result messages
  5. Change auth.Logout to return (wasLoggedIn, err) for better user feedback
Loading Success Note
Frame 48097270 Frame 48097272 Frame 480972711

Fix DES-113

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

This change adds a spinner-based UI wrapper to the logout operation and enhances the output system to support success and note event types. The logout method now returns a boolean indicating whether a user was previously logged in, enabling differentiated messaging for successful logouts versus no-token scenarios.

Changes

Cohort / File(s) Summary
Dependency Management
go.mod
Added charmbracelet packages (bubbles, bubbletea, lipgloss) and their transitive dependencies for spinner and styling functionality.
Core Logout Logic
internal/auth/auth.go
Updated Logout() signature to return (wasLoggedIn bool, err error) instead of error; now explicitly indicates whether a token existed before deletion.
Command Implementation
cmd/logout.go
Wrapped logout operation with output.RunWithSpinner() to display spinner during execution; emits differentiated messages based on logout status ("Logged out." vs "Not logged in.").
Output Event System
internal/output/events.go
Extended Event union type to include SuccessEvent and NoteEvent; added public helper functions EmitSuccess() and EmitNote().
Output Rendering
internal/output/plain_sink.go
Added Writer() method to expose underlying io.Writer; extended emit switch to handle new SuccessEvent and NoteEvent types.
Styling System
internal/output/styles.go
New file defining four exported lipgloss style variables: PrimaryStyle, SecondaryStyle, WarningStyle, and ErrorStyle for consistent UI theming.
Spinner Implementation
internal/output/spinner.go
New file providing RunWithSpinner(w io.Writer, message string, action func() error) error using bubbletea; manages spinner UI concurrent with long-running action execution.
Test Updates
test/integration/logout_test.go
Updated assertions to verify new output format with spinner indicators ("> " prompt), success message, and differentiated messaging for logged-out vs not-logged-in states.

Sequence Diagram

sequenceDiagram
    participant User
    participant LogoutCmd as Logout Command
    participant Spinner as RunWithSpinner
    participant Auth
    participant Output as Output Sink
    participant UI as Spinner UI

    User->>LogoutCmd: Execute logout
    LogoutCmd->>Spinner: RunWithSpinner(message, logout action)
    Spinner->>UI: Initialize spinner
    UI->>UI: Display spinning animation
    Spinner->>Auth: Execute Logout()
    Auth->>Auth: Check for existing token
    alt Token exists
        Auth->>Auth: Delete token
        Auth-->>Spinner: (true, nil)
    else No token
        Auth-->>Spinner: (false, nil)
    end
    Spinner->>Output: EmitSuccess/EmitNote based on wasLoggedIn
    Output->>UI: Render success/note message
    UI->>User: Display completion message
    Spinner-->>LogoutCmd: Return
    LogoutCmd->>User: Exit
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main changes: adding styled CLI output with spinner functionality to the logout command.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Description check ✅ Passed The PR description clearly relates to the changeset, detailing the addition of styled output events, spinner functionality, and updates to the logout command.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch george/des-113

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/auth/auth.go`:
- Around line 55-67: Logout currently treats any error from
tokenStorage.GetAuthToken() as "not logged in", masking real storage failures;
add a sentinel error (e.g., var ErrTokenNotFound = errors.New("token not
found")) returned by GetAuthToken() instead of a wrapped fmt.Errorf, update
GetAuthToken's implementation to return that sentinel when the underlying
keyring.ErrKeyNotFound occurs, and modify Auth.Logout() to use errors.Is(err,
ErrTokenNotFound) to return (false, nil) only for the "not found" case while
returning actual errors for other failures; keep calls to
tokenStorage.DeleteAuthToken() and the final return behavior unchanged.
🧹 Nitpick comments (1)
internal/output/styles.go (1)

5-10: Consider using AdaptiveColor consistently across all styles.

WarningStyle uses AdaptiveColor for light/dark theme adaptation, but PrimaryStyle, SecondaryStyle, and ErrorStyle use fixed colors. This may cause readability issues on light terminal backgrounds where #4DB6AC (teal) or #888888 (gray) might have poor contrast.

♻️ Optional: Use AdaptiveColor for all styles
 var (
-	PrimaryStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#4DB6AC"))
-	SecondaryStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
+	PrimaryStyle   = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#00796B", Dark: "#4DB6AC"})
+	SecondaryStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#555555", Dark: "#888888"})
 	WarningStyle   = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#2E7D32", Dark: "#FFB800"})
-	ErrorStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555"))
+	ErrorStyle     = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#C62828", Dark: "#FF5555"})
 )

Comment on lines +55 to +67
// Logout removes the stored auth token from the keyring.
// Returns true if a token was found and deleted, false if no token existed.
func (a *Auth) Logout() (wasLoggedIn bool, err error) {
_, err = a.tokenStorage.GetAuthToken()
if err != nil {
return false, nil
}

err = a.tokenStorage.DeleteAuthToken()
if err != nil {
return false, err
}
return err
return true, nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the GetAuthToken implementation and check what errors it returns
ast-grep --pattern 'func ($_ $_) GetAuthToken() ($_, $_) {
  $$$
}'

Repository: localstack/lstk

Length of output: 1179


🏁 Script executed:

# Also search for tokenStorage interface definition to understand the contract
rg -t go 'type tokenStorage|interface.*TokenStorage|type.*TokenStorage' -A 10

Repository: localstack/lstk

Length of output: 2218


🏁 Script executed:

# Search for existing error types in the auth package
rg -t go 'var.*Err|type.*Error' internal/auth/

Repository: localstack/lstk

Length of output: 41


🏁 Script executed:

# Check if there's a defined error for "credential not found" or if this is just a string
rg -t go 'credential not found|ErrTokenNotFound|ErrCredentialNotFound' internal/auth/

Repository: localstack/lstk

Length of output: 138


🏁 Script executed:

# Check if there's a way to distinguish the errors in GetAuthToken more carefully
rg -t go 'return.*credential not found' -B 5 -A 2 internal/auth/

Repository: localstack/lstk

Length of output: 546


🏁 Script executed:

# Check how Logout is tested to understand the intended behavior
rg -t go 'func.*Logout|Logout\(' internal/auth/ -A 10 | head -80

Repository: localstack/lstk

Length of output: 535


🏁 Script executed:

# Check if there's any existing pattern for handling "credential not found" in the codebase
rg -t go 'errors\.Is.*credential|errors\.Is.*Err.*Token|errors\.Is.*Err.*Found' internal/auth/

Repository: localstack/lstk

Length of output: 233


🏁 Script executed:

# Check if there are any tests for Logout() that show expected behavior
rg -t go 'func.*TestLogout|Logout.*test' internal/auth/ -A 20

Repository: localstack/lstk

Length of output: 41


Logout() masks storage errors by treating "credential not found" and actual failures identically.

The GetAuthToken() implementation at line 67 wraps keyring.ErrKeyNotFound in fmt.Errorf("credential not found"), but Logout() at line 58-60 returns (false, nil) for all errors, including real storage failures. This hides permission errors, service unavailability, and other issues behind a "not logged in" response.

To fix this, GetAuthToken() should return a defined sentinel error (e.g., var ErrTokenNotFound = errors.New("token not found")) that can be checked with errors.Is() in Logout(), allowing it to distinguish "no token" from actual storage errors.

🤖 Prompt for AI Agents
In `@internal/auth/auth.go` around lines 55 - 67, Logout currently treats any
error from tokenStorage.GetAuthToken() as "not logged in", masking real storage
failures; add a sentinel error (e.g., var ErrTokenNotFound = errors.New("token
not found")) returned by GetAuthToken() instead of a wrapped fmt.Errorf, update
GetAuthToken's implementation to return that sentinel when the underlying
keyring.ErrKeyNotFound occurs, and modify Auth.Logout() to use errors.Is(err,
ErrTokenNotFound) to return (false, nil) only for the "not found" case while
returning actual errors for other failures; keep calls to
tokenStorage.DeleteAuthToken() and the final return behavior unchanged.

@silv-io
Copy link
Member

silv-io commented Feb 16, 2026

I'd prefer to add this after we have the actual TUI in there with #38, but looks good so far :)

@gtsiolis
Copy link
Member Author

after we have the actual TUI in there with #38

Sounds good, putting this on hold. Please, ping me when I can get back to this. 🏓

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants