Skip to content

Replace profile option with loading spinner#176

Open
gtsiolis wants to merge 1 commit intomainfrom
des-190-replace-aws-cli-profile-option-with-lstk-prefix
Open

Replace profile option with loading spinner#176
gtsiolis wants to merge 1 commit intomainfrom
des-190-replace-aws-cli-profile-option-with-lstk-prefix

Conversation

@gtsiolis
Copy link
Copy Markdown
Member

@gtsiolis gtsiolis commented Mar 27, 2026

Updates the AWS CLI proxy command to show a loading spinner while waiting for a response, then automatically hide it as soon as output starts streaming.

  • Wrapped stdout/stderr writers to detect first output byte
  • Spinner stops and clears itself immediately when AWS CLI begins responding
  • Output streams in real-time (no buffering)
  • Non-interactive mode unchanged (no spinner)

See DES-190 and DRG-364 for more context.

BEFORE AFTER
Screenshot 2026-03-27 at 15 41 05 Screenshot 2026-03-27 at 15 41 11

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@gtsiolis gtsiolis self-assigned this Mar 27, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Added a new aws subcommand that proxies AWS CLI invocations to LocalStack. The command resolves the emulator endpoint by reading port configuration, constructs environment variables with AWS credentials, displays a terminal-aware spinner during execution, and invokes the AWS CLI with the injected endpoint URL and original arguments.

Changes

Cohort / File(s) Summary
AWS Command Infrastructure
cmd/aws.go, cmd/root.go
New aws subcommand that proxies CLI invocations to LocalStack; resolves endpoint via LocalStack host and configured/discovered port; registered with root command via newAWSCmd.
AWS CLI Execution
internal/awscli/exec.go, internal/awscli/spinner.go
Exec function locates AWS binary, injects endpoint URL and credentials into CLI args/environment; terminal-aware spinner provides loading feedback; BuildEnv ensures required AWS environment variables are present with defaults.
Error Handling
internal/output/errors.go
New ExitCodeError type wraps errors with exit codes; ExitCode helper extracts exit code from error chain.
Testing & Integration
internal/awscli/exec_test.go, test/integration/aws_cmd_test.go
Unit tests for environment variable construction; integration tests verify endpoint injection, credential handling, config-based port resolution, and exit code propagation.
Main Entry Point & Minor Fixes
main.go, test/integration/start_test.go
Process exit code now derived from error chain via output.ExitCode; cleanup fix for response body closure in health check test.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant LSCmd as lstk aws
    participant Config as Config/Container
    participant AWSCli as AWS CLI
    participant Spinner as Spinner

    User->>LSCmd: lstk aws [args...]
    LSCmd->>Config: resolveAWSPort()
    Config-->>LSCmd: port
    LSCmd->>LSCmd: Resolve LocalStack endpoint<br/>(host:port)
    LSCmd->>LSCmd: BuildEnv() for AWS credentials
    alt Terminal Detected
        LSCmd->>Spinner: Start spinner
    end
    LSCmd->>AWSCli: Exec with<br/>--endpoint-url + args
    AWSCli->>AWSCli: Execute command
    alt Output Received
        AWSCli-->>Spinner: Write to stdout/stderr
        Spinner->>Spinner: Stop on first write
    end
    AWSCli-->>LSCmd: Exit status
    LSCmd-->>User: Output + exit code
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • silv-io
  • anisaoshafi
  • carole-lavillonniere
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Replace profile option with loading spinner' accurately summarizes the main change: replacing an AWS CLI profile approach with a loading spinner UX.
Description check ✅ Passed The PR description accurately describes the changeset, detailing the addition of a loading spinner functionality for the AWS CLI proxy command with specific implementation details.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch des-190-replace-aws-cli-profile-option-with-lstk-prefix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 3

🧹 Nitpick comments (1)
cmd/aws.go (1)

15-25: Align user-facing help text with emulator terminology guidance.

Please update the command help to reference emulator terminology/types instead of only “LocalStack” phrasing.

As per coding guidelines, CLI/help/docs in cmd/**/*.go should reference “emulator types (aws, snowflake, azure)” with only aws currently implemented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/aws.go` around lines 15 - 25, Update the user-facing help text for the
AWS CLI command (the cobra command variable like awsCmd / the Short and Long
strings) to use the emulator terminology: refer to the emulator type "aws" (and
note emulator types such as aws, snowflake, azure) instead of only "LocalStack";
keep existing example showing endpoint and env vars but change phrases like
"Proxy AWS CLI commands to LocalStack" to "Proxy AWS CLI commands to the aws
emulator (LocalStack)" and mention that only the aws emulator type is currently
implemented. Ensure both Short and Long strings are updated accordingly to
follow cmd/**/*.go guidance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/aws.go`:
- Around line 44-47: The loop over appCfg.Containers returning c.Port for the
AWS emulator can return an empty string; update the logic in the function that
iterates appCfg.Containers (checking c.Type == config.EmulatorAWS) to guard that
c.Port is not empty before returning it—i.e., only return c.Port when both the
container type equals config.EmulatorAWS and c.Port != ""; otherwise continue
searching and if no non-empty port is found, return the configured default port
value (use the existing default constant or literal used elsewhere in the file).

In `@internal/awscli/exec.go`:
- Around line 17-27: The spinner can be stopped twice (one per stdout/stderr
wrapper) and may never be stopped if the subprocess emits no output; fix by
making Stop idempotent and by sharing a single stop action between writers and
invoking it when the process exits. Update the spinner type to guard its Stop
method with an internal sync.Once (make spinner.Stop safe for repeated calls),
and change stopOnWriteWriter construction so both instances receive the same
stop function (or a shared sync.Once) instead of calling spinner.Stop directly
in Write; also ensure the code that waits for the subprocess explicitly calls
that shared stop function after process completion so the spinner is always
stopped even when there is no output (referencing stopOnWriteWriter, its Write
method, and spinner.Stop).

In `@test/integration/aws_cmd_test.go`:
- Around line 42-43: Tests in aws_cmd_test.go currently use runLstk (non-TTY)
and a fake aws binary that only echoes, so spinner lifecycle (TTY activation and
stopping on first streamed output) isn't exercised; add a new PTY-based
integration test that uses runLstkInPTY and a fake aws binary which streams
output (writes something after a short delay) to validate that the spinner
appears while waiting and is cleared once the first output is streamed. Create a
new test function (e.g., TestAwsCmd_SpinnerLifecyclePTY) that calls
runLstkInPTY(testContext(t), t.TempDir(), e, "aws", "s3", "ls") with the
streaming fake binary and assert PTY output contains spinner characters prior to
streamed content and that spinner is removed once output begins; leave existing
runLstk-based tests (argument injection and credential propagation) unchanged.

---

Nitpick comments:
In `@cmd/aws.go`:
- Around line 15-25: Update the user-facing help text for the AWS CLI command
(the cobra command variable like awsCmd / the Short and Long strings) to use the
emulator terminology: refer to the emulator type "aws" (and note emulator types
such as aws, snowflake, azure) instead of only "LocalStack"; keep existing
example showing endpoint and env vars but change phrases like "Proxy AWS CLI
commands to LocalStack" to "Proxy AWS CLI commands to the aws emulator
(LocalStack)" and mention that only the aws emulator type is currently
implemented. Ensure both Short and Long strings are updated accordingly to
follow cmd/**/*.go guidance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 2e1d4c43-f552-4aa8-b33a-bb1ff4a6edc1

📥 Commits

Reviewing files that changed from the base of the PR and between f22a8fe and 5bcff25.

📒 Files selected for processing (9)
  • cmd/aws.go
  • cmd/root.go
  • internal/awscli/exec.go
  • internal/awscli/exec_test.go
  • internal/awscli/spinner.go
  • internal/output/errors.go
  • main.go
  • test/integration/aws_cmd_test.go
  • test/integration/start_test.go

Comment on lines +44 to +47
for _, c := range appCfg.Containers {
if c.Type == config.EmulatorAWS {
return c.Port
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against empty AWS port config before returning it.

If the AWS container exists but c.Port is empty, Line 46 returns "", which can yield an invalid endpoint URL. Fall back to default in that case.

Proposed fix
 func resolveAWSPort() string {
 	const defaultPort = "4566"
@@
 	for _, c := range appCfg.Containers {
 		if c.Type == config.EmulatorAWS {
-			return c.Port
+			if c.Port != "" {
+				return c.Port
+			}
+			return defaultPort
 		}
 	}
 	return defaultPort
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, c := range appCfg.Containers {
if c.Type == config.EmulatorAWS {
return c.Port
}
func resolveAWSPort() string {
const defaultPort = "4566"
for _, c := range appCfg.Containers {
if c.Type == config.EmulatorAWS {
if c.Port != "" {
return c.Port
}
return defaultPort
}
}
return defaultPort
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/aws.go` around lines 44 - 47, The loop over appCfg.Containers returning
c.Port for the AWS emulator can return an empty string; update the logic in the
function that iterates appCfg.Containers (checking c.Type == config.EmulatorAWS)
to guard that c.Port is not empty before returning it—i.e., only return c.Port
when both the container type equals config.EmulatorAWS and c.Port != "";
otherwise continue searching and if no non-empty port is found, return the
configured default port value (use the existing default constant or literal used
elsewhere in the file).

Comment on lines +17 to +27
type stopOnWriteWriter struct {
w io.Writer
spinner *spinner
once sync.Once
}

func (s *stopOnWriteWriter) Write(p []byte) (int, error) {
s.once.Do(func() {
s.spinner.Stop()
})
return s.w.Write(p)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix spinner lifecycle: possible panic + leaked spinner when no output.

Current logic allows two independent stop paths (stdout/stderr wrappers), so Stop() can be invoked twice. Also, if the subprocess exits without writing output, the spinner is never stopped.

Proposed fix
 type stopOnWriteWriter struct {
-	w       io.Writer
-	spinner *spinner
-	once    sync.Once
+	w    io.Writer
+	stop func()
 }
 
 func (s *stopOnWriteWriter) Write(p []byte) (int, error) {
-	s.once.Do(func() {
-		s.spinner.Stop()
-	})
+	s.stop()
 	return s.w.Write(p)
 }
@@
 	var s *spinner
 	if isTerminal(os.Stderr) {
 		s = newSpinner(os.Stderr, "Loading...")
 		s.Start()
+		var stopOnce sync.Once
+		stopSpinner := func() {
+			stopOnce.Do(func() { s.Stop() })
+		}
+		defer stopSpinner()
 
 		// Wrap stdout/stderr to stop spinner on first output
-		stopWriter := &stopOnWriteWriter{w: os.Stdout, spinner: s}
-		cmd.Stdout = stopWriter
-		cmd.Stderr = &stopOnWriteWriter{w: os.Stderr, spinner: s}
+		cmd.Stdout = &stopOnWriteWriter{w: os.Stdout, stop: stopSpinner}
+		cmd.Stderr = &stopOnWriteWriter{w: os.Stderr, stop: stopSpinner}
 	} else {
 		cmd.Stdout = os.Stdout
 		cmd.Stderr = os.Stderr
 	}

Also applies to: 44-56, 58-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/awscli/exec.go` around lines 17 - 27, The spinner can be stopped
twice (one per stdout/stderr wrapper) and may never be stopped if the subprocess
emits no output; fix by making Stop idempotent and by sharing a single stop
action between writers and invoking it when the process exits. Update the
spinner type to guard its Stop method with an internal sync.Once (make
spinner.Stop safe for repeated calls), and change stopOnWriteWriter construction
so both instances receive the same stop function (or a shared sync.Once) instead
of calling spinner.Stop directly in Write; also ensure the code that waits for
the subprocess explicitly calls that shared stop function after process
completion so the spinner is always stopped even when there is no output
(referencing stopOnWriteWriter, its Write method, and spinner.Stop).

Comment on lines +42 to +43
stdout, stderr, err := runLstk(t, testContext(t), t.TempDir(), e, "aws", "s3", "ls")
require.NoError(t, err, "lstk aws failed: %s", stderr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify whether aws integration coverage includes PTY/spinner assertions.
rg -nP 'runLstkInPTY\(|runLstk\(|Loading|spinner' test/integration/aws_cmd_test.go test/integration/main_test.go

Repository: localstack/lstk

Length of output: 1085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check runLstkInPTY implementation
echo "=== runLstkInPTY Implementation ==="
sed -n '236,280p' test/integration/main_test.go

# Check if any integration tests use runLstkInPTY
echo ""
echo "=== Other Tests Using runLstkInPTY ==="
rg -l 'runLstkInPTY' test/integration/ || echo "No other tests found using runLstkInPTY"

# Look for spinner-related code in cmd/
echo ""
echo "=== Spinner-Related Code in cmd/ ==="
rg -n 'spinner|Loading' cmd/ --type go | head -30

# Check the aws command implementation
echo ""
echo "=== AWS Command Implementation ==="
fd -e go 'aws.*\.go' cmd/ | xargs grep -l "spinner\|Loading" || echo "No spinner found in aws command files"

Repository: localstack/lstk

Length of output: 1441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all aws command related files
echo "=== AWS Command Files ==="
fd -e go 'aws' cmd/ -t f

# Check if spinner is part of output streaming in aws command
echo ""
echo "=== Check AWS S3 Command Implementation ==="
rg -n 'func.*aws.*s3\|func.*S3' cmd/ -A 20 | head -50

Repository: localstack/lstk

Length of output: 140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the aws command file completely
echo "=== aws.go Content ==="
wc -l cmd/aws.go

# If it's reasonable size, read it all
if [ $(wc -l < cmd/aws.go) -lt 200 ]; then
  cat -n cmd/aws.go
else
  echo "File is large, reading sections..."
fi

Repository: localstack/lstk

Length of output: 1842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any streaming output or Bubble Tea related code
echo "=== Search for Bubble Tea/Tea imports ==="
rg -n 'tea\.|bubbletea|Bubble Tea' cmd/ --type go

# Search for output sink or output mode selection
echo ""
echo "=== Search for output sink selection ==="
rg -n 'PlainSink|TTY|interactive|output\.' cmd/aws.go -B 2 -A 2

# Check what output handling is done in the actual aws s3 ls command
echo ""
echo "=== Full aws.go to see output handling ==="
cat cmd/aws.go

Repository: localstack/lstk

Length of output: 100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and check awscli package
echo "=== Find awscli files ==="
fd -e go 'awscli' internal/

# Search for spinner in internal/awscli
echo ""
echo "=== Search for spinner/Bubble Tea in awscli ==="
rg -n 'spinner|tea\.|bubbletea|Loading' internal/awscli/ --type go || echo "No spinner found in awscli"

# Check the awscli.Exec function
echo ""
echo "=== Check Exec function signature ==="
rg -n 'func Exec' internal/awscli/ -A 5

Repository: localstack/lstk

Length of output: 1846


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the entire aws_cmd_test.go file to understand test structure
echo "=== aws_cmd_test.go Content ==="
cat -n test/integration/aws_cmd_test.go

Repository: localstack/lstk

Length of output: 5217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read exec.go to understand spinner activation condition
echo "=== internal/awscli/exec.go ==="
cat -n internal/awscli/exec.go

# Check if there are any existing spinner tests
echo ""
echo "=== Search for spinner tests ==="
rg -n 'spinner\|Loading' test/ --type go

Repository: localstack/lstk

Length of output: 2706


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

# Find isTerminal function definition
echo "=== Search for isTerminal function ==="
rg -n 'func isTerminal' internal/awscli/ -A 5

# Check if there are any other places where spinner is used or tested
echo ""
echo "=== Check spinner usage in entire codebase ==="
rg -n 'newSpinner\|spinner\.' --type go | grep -v test | grep -v '.go:.*// ' || echo "No additional spinner usage found"

# Verify that the review comment's claim about non-interactive spinner assertions
echo ""
echo "=== Verify stopOnWriteWriter implementation ==="
sed -n '16,28p' internal/awscli/exec.go

Repository: localstack/lstk

Length of output: 942


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what isTerminal does
fd -e go . internal/ | xargs grep -l 'isTerminal' | head -5

# Look for terminal detection helper
rg -n 'isTerminal\|IsTerminal' --type go | head -20

Repository: localstack/lstk

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the full isTerminal implementation
echo "=== Full isTerminal implementation ==="
sed -n '74,90p' internal/awscli/spinner.go

# Check if there are any other tests that validate spinner behavior
echo ""
echo "=== Check for spinner-related assertions in tests ==="
rg -n 'Loading\|spinner\|Stop\|Start' test/integration/ --type go -B 2 -A 2

# Understand what the learnings ask - check if any cmd tests use runLstkInPTY
echo ""
echo "=== Check all cmd tests for PTY usage ==="
fd -e go 'cmd.*test.go' test/integration/ | xargs grep -l 'runLstkInPTY\|PTY' || echo "No cmd tests use PTY"

Repository: localstack/lstk

Length of output: 346


Add a PTY-based integration test for spinner lifecycle in AWS command.

The spinner behavior (activate on TTY, stop on first streamed output) is not tested. All existing aws_cmd_test.go tests use runLstk(), which executes in non-interactive mode, and employ a fake aws binary that only echoes arguments—neither condition triggers or validates spinner behavior.

A dedicated regression test should use runLstkInPTY() with a fake aws binary that streams output to verify the spinner displays while waiting and clears when output begins. The existing tests (lines 42-43, 53-54, 69-70, 80-81, 101-102, 130-131) validating argument injection and credential propagation do not require conversion to PTY.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/aws_cmd_test.go` around lines 42 - 43, Tests in
aws_cmd_test.go currently use runLstk (non-TTY) and a fake aws binary that only
echoes, so spinner lifecycle (TTY activation and stopping on first streamed
output) isn't exercised; add a new PTY-based integration test that uses
runLstkInPTY and a fake aws binary which streams output (writes something after
a short delay) to validate that the spinner appears while waiting and is cleared
once the first output is streamed. Create a new test function (e.g.,
TestAwsCmd_SpinnerLifecyclePTY) that calls runLstkInPTY(testContext(t),
t.TempDir(), e, "aws", "s3", "ls") with the streaming fake binary and assert PTY
output contains spinner characters prior to streamed content and that spinner is
removed once output begins; leave existing runLstk-based tests (argument
injection and credential propagation) unchanged.

Copy link
Copy Markdown
Contributor

@anisaoshafi anisaoshafi left a comment

Choose a reason for hiding this comment

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

Thanks George for investing in this nice feature ⭐

  1. Issue: The spinner behaviour is weird, it gets stuck e.g:
  • a) when you try lstk aws s3 ls when you have no bucket
  • b) create a bucket and then delete bucket. I suggest to revisit it to make it more robust 🙏🏼
Image Image
  1. Suggestion: would be nice to add a more descriptive message when LocalStack is not running:
Image

Comment on lines +7 to +19
"strings"
"sync"
"time"
)

var dotFrames = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}

// ANSI color codes matching lstk's spinner style (color 69 blue) and secondary (color 241 gray)
const (
spinnerColor = "\033[38;5;69m"
secondaryColor = "\033[38;5;241m"
resetColor = "\033[0m"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: didn't we have these properties defined somewhere for Nimbo? I think they're duplicated.

Also, In components package, we have spinner defined. Why are we redefining it again in this PR, can you try reuse them?

Btw 🎗️, we have this /review-pr claude skill: /review-pr #176 that the team added, and it can be run in local development to catch this architectural pattern issues 🙏🏼

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants