Skip to content

Migrate stop command to output event system#71

Merged
gtsiolis merged 4 commits intomainfrom
george/compact-stop-command-output
Mar 4, 2026
Merged

Migrate stop command to output event system#71
gtsiolis merged 4 commits intomainfrom
george/compact-stop-command-output

Conversation

@gtsiolis
Copy link
Member

@gtsiolis gtsiolis commented Mar 3, 2026

See DES-152 for more context.

** header can be hidden once #69 is merged.

BEFORE AFTER
Screenshot 2026-03-04 at 01 10 57 Screenshot 2026-03-04 at 01 09 04
Screenshot 2026-03-04 at 01 10 58 Screenshot 2026-03-04 at 01 08 38

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Warning

Rate limit exceeded

@gtsiolis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: 7c765228-6475-4921-9b4d-7096f96051f2

📥 Commits

Reviewing files that changed from the base of the PR and between 5513559 and 7f3e3cc.

📒 Files selected for processing (1)
  • internal/ui/run_stop.go
📝 Walkthrough

Walkthrough

Replaces callback-based progress handling with an output.Sink, updates container.Stop signature, and adds an interactive TUI path (ui.RunStop) invoked from cmd/stop.go; non-interactive fallback uses a plain stdout sink.

Changes

Cohort / File(s) Summary
Stop Command Integration
cmd/stop.go
Add interactive branch: if UI is interactive call ui.RunStop(ctx, rt) and return early on error; otherwise create output.NewPlainSink(os.Stdout) and call container.Stop with the sink. Added imports for output and ui.
Container Stop Refactor
internal/container/stop.go
Change Stop signature to accept output.Sink instead of onProgress func(string). Replace callback usage with sink-based spinner and success messages; check rt.IsRunning before stopping; unified error handling and spinner lifecycle.
Interactive UI Handler
internal/ui/run_stop.go
New RunStop function implementing a Bubble Tea TUI: runs container.Stop in a background goroutine with a TUI sink, streams status to the UI, handles cancellation and propagates non-canceled errors.
Runtime check tweak
internal/runtime/docker.go
IsRunning now returns false, nil when inspect returns not-found instead of propagating the error.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Cmd as cmd/stop
    participant UI as ui.RunStop
    participant Container as internal/container.Stop
    participant Sink as output.Sink

    User->>Cmd: invoke stop command
    alt UI is interactive
        Cmd->>UI: RunStop(ctx, rt)
        UI->>UI: init Bubble Tea app
        UI->>Container: Stop(ctx, rt, tuiSink) (background)
        Container->>Sink: EmitSpinnerStart
        Container->>Container: rt.IsRunning?
        alt running
            Container->>Container: stop container
            Container->>Sink: EmitSpinnerStop
            Container->>Sink: EmitSuccess
            Container-->>UI: status updates
        else not running / error
            Container->>Sink: EmitSpinnerStop
            Container-->>UI: error status
        end
        UI->>User: render interactive progress/results
    else non-interactive
        Cmd->>Container: Stop(ctx, rt, plainSink)
        Container->>Sink: EmitSpinnerStart/Stop and messages (stdout)
        Container-->>Cmd: return result
    end
    Cmd-->>User: exit
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Add stop command #21: Prior implementation that added container.Stop(ctx, rt, onProgress) and the CLI stop command; related because this PR changes the Stop signature and progress handling.

Suggested reviewers

  • silv-io
  • carole-lavillonniere
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Migrate stop command to output event system' clearly and concisely describes the main change: refactoring the stop command to use a new output event system instead of the previous callback-based approach.
Description check ✅ Passed The description references a related issue (DES-152), acknowledges a dependency, and provides before/after screenshots demonstrating the UI changes from plain text output to an interactive Bubble Tea UI with spinner and success messages.

✏️ 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/compact-stop-command-output

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
Collaborator

@carole-lavillonniere carole-lavillonniere left a comment

Choose a reason for hiding this comment

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

👏

@gtsiolis gtsiolis force-pushed the george/compact-stop-command-output branch from 4e1eda3 to 5bbde72 Compare March 4, 2026 09:39
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: 3

🤖 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/stop.go`:
- Around line 43-45: The init() function in cmd/stop.go attempts to call
rootCmd.AddCommand(newStopCmd()) but rootCmd is undefined in this package;
replace the incorrect registration by adding the stop command to the actual root
command symbol used in this module (or restore the previous registration path).
Locate the package's real root command (e.g., a symbol named RootCmd,
GetRootCmd(), or the central command initializer) and change init() to call that
symbol's AddCommand with newStopCmd(), or alternatively export newStopCmd() and
register it where the real root is constructed; ensure init() references the
correct identifier instead of rootCmd so the package compiles.

In `@internal/container/stop.go`:
- Around line 20-23: The current check `err != nil || !running` in the IsRunning
call masks real runtime errors by always returning "%s is not running"; change
it to handle the two outcomes separately: if `err != nil` return the underlying
error (wrapped for context, e.g. fmt.Errorf("checking %s running: %w", name,
err)), and only if `err == nil && !running` return fmt.Errorf("%s is not
running", name). Locate the call to `rt.IsRunning(ctx, name)` in stop.go and
update the conditional to preserve and wrap the original error instead of
collapsing both cases.

In `@internal/ui/run_stop.go`:
- Around line 15-16: The derived cancelable context created by
context.WithCancel(parentCtx) is never used; replace uses of parentCtx with the
derived ctx (the variable returned alongside cancel) so UI-triggered cancel will
propagate to container.Stop and any background waiter; specifically, pass the
derived ctx into container.Stop (instead of parentCtx) and into the goroutine or
wait logic that waits on the done channel (the code around the container.Stop
call and the later wait at the done/wait block), keep defer cancel() to ensure
cleanup.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: 5a2e2128-38e6-48fa-99a3-bb10e9fbcc19

📥 Commits

Reviewing files that changed from the base of the PR and between 72c868a and 5bbde72.

📒 Files selected for processing (3)
  • cmd/stop.go
  • internal/container/stop.go
  • internal/ui/run_stop.go

@gtsiolis gtsiolis force-pushed the george/compact-stop-command-output branch from 5bbde72 to 2be6f47 Compare March 4, 2026 11:21
@gtsiolis
Copy link
Member Author

gtsiolis commented Mar 4, 2026

@coderabbitai left some review comments I appended in 2be6f47.

@carole-lavillonniere Could you take another look when you're free?

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

@gtsiolis Sure, I'll take another look at the PR including the latest changes in 2be6f47!

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

The PR description images show a clear before/after comparison of the stop command output:

  • Before: Plain text output (Stopping localstack-aws ...localstack-aws stopped)
  • After: A Bubble Tea TUI with the lstk mascot header, an animated spinner during the stop operation, and a green > Success: localstack-aws stopped message on completion

@gtsiolis Sure! I'll take another look at the PR including the latest changes in 2be6f47.

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch PR review comments
gh pr view 71 --repo localstack/lstk --json reviews,comments,reviewRequests 2>/dev/null || echo "Access issue"

# Try fetching review comments via API
gh api repos/localstack/lstk/pulls/71/comments 2>/dev/null | jq '[.[] | {id, body, path, line, commit_id, created_at}]' || echo "API access issue"

# Fetch commit details for 2be6f47
gh api repos/localstack/lstk/commits/2be6f47 2>/dev/null | jq '{sha, commit: {message: .commit.message}}' || echo "Commit access issue"

Length of output: 25690


Let me check the current state of cmd/stop.go to verify the build-breaking rootCmd issue.


🏁 Script executed:

#!/bin/bash
# Check current state of cmd/stop.go
cat cmd/stop.go 2>/dev/null || fd -e go stop cmd/ --exec cat {}

# Also check how other commands register themselves to understand the pattern
fd -e go . cmd/ --exec grep -l "AddCommand\|rootCmd\|RootCmd" {}

Length of output: 953


🏁 Script executed:

#!/bin/bash
# Check how root.go registers commands
cat cmd/root.go

Length of output: 1960


@gtsiolis All three review comments have been addressed in commit 2be6f47:

  1. cmd/stop.gorootCmd undefined (Critical) ✅ — The broken init() function was removed from stop.go. newStopCmd() is now correctly registered in cmd/root.go via NewRootCmd()root.AddCommand(newStopCmd(), ...), which resolves the build failure.

  2. internal/container/stop.goIsRunning error masking (Major) ✅ — The collapsed err != nil || !running check was split to correctly distinguish infrastructure errors from "not running" state.

  3. internal/ui/run_stop.go — derived context unused (Critical) ✅ — The derived cancelable context (runCtx) is now correctly threaded into container.Stop, so UI-triggered cancellation properly propagates to the background goroutine.

I'll trigger a full review on the updated changes now.

✅ Actions performed

Full review triggered.

@gtsiolis gtsiolis force-pushed the george/compact-stop-command-output branch from 2be6f47 to 52119a0 Compare March 4, 2026 19:47
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

🧹 Nitpick comments (2)
internal/container/stop.go (1)

18-26: Early return on first non-running container may leave other containers running.

When iterating multiple containers, if one is not running, the function returns immediately without attempting to stop subsequent containers. This could leave the system in a partial state. Consider whether you want to:

  1. Continue stopping remaining containers and collect errors, or
  2. Change to a warning/skip pattern for containers that aren't running

If the current behavior is intentional (fail-fast), this is fine as-is.

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

In `@internal/container/stop.go` around lines 18 - 26, The loop over
cfg.Containers returns immediately if any container is not running, which leaves
later containers unprocessed; modify the logic in the loop that calls
rt.IsRunning(ctx, name) (and uses name := c.Name()) to either skip non-running
containers with a warning (e.g., log via provided logger) or, preferably,
continue processing all containers while collecting errors into a slice (or
errors.Join) so you attempt to stop every container and then return a combined
error at the end; update the early return paths that check running state to
append to the error collection and continue, and after the loop return a single
aggregated error (or nil) instead of failing fast.
internal/ui/run_stop.go (1)

22-30: Consider using defer for channel send consistency with run.go.

The error handling works correctly, but differs from the pattern in internal/ui/run.go which uses defer to ensure the channel send always happens. While not a bug, aligning with the existing pattern improves maintainability.

♻️ Optional: Align with run.go pattern
 	go func() {
+		var err error
+		defer func() { runErrCh <- err }()
 		err := container.Stop(ctx, rt, output.NewTUISink(programSender{p: p}))
-		runErrCh <- err
 		if err != nil && !errors.Is(err, context.Canceled) {
 			p.Send(runErrMsg{err: err})
 			return
 		}
 		p.Send(runDoneMsg{})
 	}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ui/run_stop.go` around lines 22 - 30, The goroutine currently sends
runErrCh and p.Send conditionally; change it to follow the run.go pattern by
using defer to always send the error to runErrCh: call err :=
container.Stop(ctx, rt, output.NewTUISink(programSender{p: p})), then
immediately defer sending err into runErrCh (defer func(){ runErrCh <- err }()),
after which check if err != nil && !errors.Is(err, context.Canceled) to
p.Send(runErrMsg{err: err}) and return, otherwise p.Send(runDoneMsg{}); use the
existing symbols container.Stop, programSender, runErrCh, runErrMsg, runDoneMsg,
and p.Send to locate and update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/ui/run_stop.go`:
- Around line 18-19: The stop UI currently constructs app := NewApp("", "", "",
cancel) which leaves blank header lines; mirror RunLogout by suppressing the
header or supply real version/emulator/endpoint values—specifically, change the
creation to call withoutHeader() on the app (e.g., NewApp("", "", "",
cancel).withoutHeader()) so the header is hidden consistently with RunLogout;
alternatively, populate the version, emulatorName, and endpoint arguments
instead of empty strings if you want the header shown.

---

Nitpick comments:
In `@internal/container/stop.go`:
- Around line 18-26: The loop over cfg.Containers returns immediately if any
container is not running, which leaves later containers unprocessed; modify the
logic in the loop that calls rt.IsRunning(ctx, name) (and uses name := c.Name())
to either skip non-running containers with a warning (e.g., log via provided
logger) or, preferably, continue processing all containers while collecting
errors into a slice (or errors.Join) so you attempt to stop every container and
then return a combined error at the end; update the early return paths that
check running state to append to the error collection and continue, and after
the loop return a single aggregated error (or nil) instead of failing fast.

In `@internal/ui/run_stop.go`:
- Around line 22-30: The goroutine currently sends runErrCh and p.Send
conditionally; change it to follow the run.go pattern by using defer to always
send the error to runErrCh: call err := container.Stop(ctx, rt,
output.NewTUISink(programSender{p: p})), then immediately defer sending err into
runErrCh (defer func(){ runErrCh <- err }()), after which check if err != nil &&
!errors.Is(err, context.Canceled) to p.Send(runErrMsg{err: err}) and return,
otherwise p.Send(runDoneMsg{}); use the existing symbols container.Stop,
programSender, runErrCh, runErrMsg, runDoneMsg, and p.Send to locate and update
the code.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: 8926c43e-ada0-4e38-a780-b1955e0c94d3

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbde72 and 52119a0.

📒 Files selected for processing (3)
  • cmd/stop.go
  • internal/container/stop.go
  • internal/ui/run_stop.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/stop.go

@gtsiolis gtsiolis merged commit 206a725 into main Mar 4, 2026
8 checks passed
@gtsiolis gtsiolis deleted the george/compact-stop-command-output branch March 4, 2026 20:05
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