From b646e3f3f80cafa6fdbb7ef02bf131c1dbc76325 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Tue, 28 Apr 2026 16:45:22 +0300 Subject: [PATCH 1/4] Use sink for already logged in output --- cmd/login.go | 21 +++++++++++++++++---- internal/output/plain_format.go | 10 ++++++---- internal/ui/styles/styles.go | 2 +- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 582cc404..8add485d 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -2,11 +2,13 @@ package cmd import ( "fmt" + "os" "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" "github.com/localstack/lstk/internal/env" "github.com/localstack/lstk/internal/log" + "github.com/localstack/lstk/internal/output" "github.com/localstack/lstk/internal/telemetry" "github.com/localstack/lstk/internal/ui" "github.com/localstack/lstk/internal/version" @@ -23,14 +25,25 @@ func newLoginCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra. if !isInteractiveMode(cfg) { return fmt.Errorf("login requires an interactive terminal") } + tokenStorage, err := auth.NewTokenStorage(cfg.ForceFileKeyring, logger) + if err != nil { + return fmt.Errorf("failed to initialize token storage: %w", err) + } + sink := output.NewPlainSink(os.Stdout) + if cfg.AuthToken != "" { + sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) + return nil + } + if token, err := tokenStorage.GetAuthToken(); err == nil && token != "" { + sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) + return nil + } platformClient := api.NewPlatformClient(cfg.APIEndpoint, logger) if err := ui.RunLogin(cmd.Context(), version.Version(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, logger); err != nil { return err } - if tokenStorage, err := auth.NewTokenStorage(cfg.ForceFileKeyring, logger); err == nil { - if token, err := tokenStorage.GetAuthToken(); err == nil && token != "" { - tel.SetAuthToken(token) - } + if token, err := tokenStorage.GetAuthToken(); err == nil && token != "" { + tel.SetAuthToken(token) } return nil }, diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index c7ad1eda..7a1e077c 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" "time" + + "github.com/localstack/lstk/internal/ui/styles" ) // FormatEventLine converts an output event into a single display line. @@ -118,13 +120,13 @@ func formatAuthEvent(e AuthEvent) string { func formatMessageEvent(e MessageEvent) string { switch e.Severity { case SeveritySuccess: - return SuccessMarker() + " " + e.Text + return styles.Success.Render(SuccessMarker()) + " " + e.Text case SeverityNote: - return "> Note: " + e.Text + return styles.Secondary.Render(">") + " " + styles.Note.Render("Note:") + " " + e.Text case SeverityWarning: - return "> Warning: " + e.Text + return styles.Secondary.Render(">") + " " + styles.Warning.Render("Warning:") + " " + e.Text case SeveritySecondary: - return e.Text + return styles.SecondaryMessage.Render(e.Text) default: return e.Text } diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index b71191f6..9dcbbc2c 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -45,7 +45,7 @@ var ( Foreground(lipgloss.Color(SuccessColor)) Note = lipgloss.NewStyle(). - Foreground(lipgloss.Color("33")) + Foreground(lipgloss.Color("69")) Warning = lipgloss.NewStyle(). Foreground(lipgloss.Color("214")) From 4193d24aa8c9e4d1fabb4fffcdd76742641d1cca Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Thu, 7 May 2026 12:31:42 +0300 Subject: [PATCH 2/4] Keep plain output unstyled --- cmd/login.go | 8 ++------ internal/output/plain_format.go | 10 ++++------ internal/ui/run.go | 7 +++++++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 8add485d..c286b1e5 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" @@ -29,14 +28,11 @@ func newLoginCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra. if err != nil { return fmt.Errorf("failed to initialize token storage: %w", err) } - sink := output.NewPlainSink(os.Stdout) if cfg.AuthToken != "" { - sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) - return nil + return ui.RunMessage(cmd.Context(), output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) } if token, err := tokenStorage.GetAuthToken(); err == nil && token != "" { - sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) - return nil + return ui.RunMessage(cmd.Context(), output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) } platformClient := api.NewPlatformClient(cfg.APIEndpoint, logger) if err := ui.RunLogin(cmd.Context(), version.Version(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, logger); err != nil { diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index 7a1e077c..c7ad1eda 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -4,8 +4,6 @@ import ( "fmt" "strings" "time" - - "github.com/localstack/lstk/internal/ui/styles" ) // FormatEventLine converts an output event into a single display line. @@ -120,13 +118,13 @@ func formatAuthEvent(e AuthEvent) string { func formatMessageEvent(e MessageEvent) string { switch e.Severity { case SeveritySuccess: - return styles.Success.Render(SuccessMarker()) + " " + e.Text + return SuccessMarker() + " " + e.Text case SeverityNote: - return styles.Secondary.Render(">") + " " + styles.Note.Render("Note:") + " " + e.Text + return "> Note: " + e.Text case SeverityWarning: - return styles.Secondary.Render(">") + " " + styles.Warning.Render("Warning:") + " " + e.Text + return "> Warning: " + e.Text case SeveritySecondary: - return styles.SecondaryMessage.Render(e.Text) + return e.Text default: return e.Text } diff --git a/internal/ui/run.go b/internal/ui/run.go index f176b7cb..969817a1 100644 --- a/internal/ui/run.go +++ b/internal/ui/run.go @@ -98,6 +98,13 @@ func Run(parentCtx context.Context, runOpts RunOptions) error { return nil } +func RunMessage(parentCtx context.Context, event output.MessageEvent) error { + return runWithTUI(parentCtx, withoutHeader(), func(ctx context.Context, sink output.Sink) error { + sink.Emit(event) + return nil + }) +} + func IsInteractive() bool { return term.IsTerminal(int(os.Stdout.Fd())) && term.IsTerminal(int(os.Stdin.Fd())) } From 5d0e018b5a9533ffb44d2dee401bbb3cf75e9343 Mon Sep 17 00:00:00 2001 From: carole-lavillonniere Date: Thu, 7 May 2026 14:56:37 +0200 Subject: [PATCH 3/4] remove redundant condition --- cmd/login.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index c286b1e5..b77beda7 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -28,10 +28,8 @@ func newLoginCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra. if err != nil { return fmt.Errorf("failed to initialize token storage: %w", err) } - if cfg.AuthToken != "" { - return ui.RunMessage(cmd.Context(), output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) - } - if token, err := tokenStorage.GetAuthToken(); err == nil && token != "" { + storedToken, _ := tokenStorage.GetAuthToken() + if cfg.AuthToken != "" || storedToken != "" { return ui.RunMessage(cmd.Context(), output.MessageEvent{Severity: output.SeverityNote, Text: "You're already logged in"}) } platformClient := api.NewPlatformClient(cfg.APIEndpoint, logger) From 3a92386fe0e5b32eb187134598b9c7588ef07aab Mon Sep 17 00:00:00 2001 From: carole-lavillonniere Date: Thu, 7 May 2026 14:56:43 +0200 Subject: [PATCH 4/4] add integration tests --- test/integration/login_test.go | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/integration/login_test.go b/test/integration/login_test.go index c912526d..d4be99ad 100644 --- a/test/integration/login_test.go +++ b/test/integration/login_test.go @@ -8,7 +8,9 @@ import ( "io" "net/http" "net/http/httptest" + "os" "os/exec" + "path/filepath" "runtime" "testing" "time" @@ -194,3 +196,40 @@ func TestDeviceFlowFailure_RequestNotConfirmed(t *testing.T) { assert.Error(t, err, "no token should be stored when login fails") assertCommandTelemetry(t, events, "login", 1) } + +func TestLoginShortCircuitsWhenEnvTokenSet(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + t.Parallel() + + environ := append(testEnvWithHome(t.TempDir(), ""), string(env.AuthToken)+"=fake-env-token") + + out, err := runLstkInPTY(t, testContext(t), environ, "login") + require.NoError(t, err, "login should succeed when env token is set: %s", out) + requireExitCode(t, 0, err) + assert.Contains(t, out, "You're already logged in") + assert.NotContains(t, out, "Opening browser") + assert.NotContains(t, out, "Waiting for authorization") +} + +func TestLoginShortCircuitsWhenStoredTokenExists(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + t.Parallel() + + tmpHome := t.TempDir() + tokenDir := filepath.Join(tmpHome, ".config", "lstk") + require.NoError(t, os.MkdirAll(tokenDir, 0700)) + require.NoError(t, os.WriteFile(filepath.Join(tokenDir, "auth-token"), []byte("stored-token"), 0600)) + + environ := env.Environ(testEnvWithHome(tmpHome, "")).Without(env.AuthToken) + + out, err := runLstkInPTY(t, testContext(t), environ, "login") + require.NoError(t, err, "login should succeed when stored token exists: %s", out) + requireExitCode(t, 0, err) + assert.Contains(t, out, "You're already logged in") + assert.NotContains(t, out, "Opening browser") + assert.NotContains(t, out, "Waiting for authorization") +}