From f75e8f159132444fea6cdbeca28cd5a6b995f588 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 4 Mar 2026 01:10:07 +0200 Subject: [PATCH 1/4] Migrate stop command to output event system --- cmd/stop.go | 16 ++++++++++--- internal/container/stop.go | 17 ++++++++------ internal/ui/run_stop.go | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 internal/ui/run_stop.go diff --git a/cmd/stop.go b/cmd/stop.go index aaa5c4d..480a46e 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -5,7 +5,9 @@ import ( "os" "github.com/localstack/lstk/internal/container" + "github.com/localstack/lstk/internal/output" "github.com/localstack/lstk/internal/runtime" + "github.com/localstack/lstk/internal/ui" "github.com/spf13/cobra" ) @@ -22,14 +24,22 @@ func newStopCmd() *cobra.Command { os.Exit(1) } - onProgress := func(msg string) { - fmt.Println(msg) + if ui.IsInteractive() { + if err := ui.RunStop(cmd.Context(), rt); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return } - if err := container.Stop(cmd.Context(), rt, onProgress); err != nil { + if err := container.Stop(cmd.Context(), rt, output.NewPlainSink(os.Stdout)); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }, } } + +func init() { + rootCmd.AddCommand(newStopCmd()) +} diff --git a/internal/container/stop.go b/internal/container/stop.go index cdac02d..7af4319 100644 --- a/internal/container/stop.go +++ b/internal/container/stop.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/containerd/errdefs" "github.com/localstack/lstk/internal/config" + "github.com/localstack/lstk/internal/output" "github.com/localstack/lstk/internal/runtime" ) -func Stop(ctx context.Context, rt runtime.Runtime, onProgress func(string)) error { +func Stop(ctx context.Context, rt runtime.Runtime, sink output.Sink) error { cfg, err := config.Get() if err != nil { return fmt.Errorf("failed to get config: %w", err) @@ -17,14 +17,17 @@ func Stop(ctx context.Context, rt runtime.Runtime, onProgress func(string)) erro for _, c := range cfg.Containers { name := c.Name() - onProgress(fmt.Sprintf("Stopping %s...", name)) + running, err := rt.IsRunning(ctx, name) + if err != nil || !running { + return fmt.Errorf("%s is not running", name) + } + output.EmitSpinnerStart(sink, fmt.Sprintf("Stopping %s", name)) if err := rt.Stop(ctx, name); err != nil { - if errdefs.IsNotFound(err) { - return fmt.Errorf("%s is not running", name) - } + output.EmitSpinnerStop(sink) return fmt.Errorf("failed to stop %s: %w", name, err) } - onProgress(fmt.Sprintf("%s stopped", name)) + output.EmitSpinnerStop(sink) + output.EmitSuccess(sink, fmt.Sprintf("%s stopped", name)) } return nil diff --git a/internal/ui/run_stop.go b/internal/ui/run_stop.go new file mode 100644 index 0000000..b917a93 --- /dev/null +++ b/internal/ui/run_stop.go @@ -0,0 +1,47 @@ +package ui + +import ( + "context" + "errors" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/localstack/lstk/internal/container" + "github.com/localstack/lstk/internal/output" + "github.com/localstack/lstk/internal/runtime" +) + +func RunStop(parentCtx context.Context, rt runtime.Runtime) error { + _, cancel := context.WithCancel(parentCtx) + defer cancel() + + app := NewApp("", "", "", cancel) + p := tea.NewProgram(app, tea.WithInput(os.Stdin), tea.WithOutput(os.Stdout)) + runErrCh := make(chan error, 1) + + go func() { + err := container.Stop(parentCtx, 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{}) + }() + + model, err := p.Run() + if err != nil { + return err + } + + if app, ok := model.(App); ok && app.Err() != nil { + return app.Err() + } + + runErr := <-runErrCh + if runErr != nil && !errors.Is(runErr, context.Canceled) { + return runErr + } + + return nil +} From 52119a074efb110db6c46e37b82a2ff9e75ecefa Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 4 Mar 2026 13:19:56 +0200 Subject: [PATCH 2/4] Review comments --- cmd/stop.go | 3 --- internal/container/stop.go | 5 ++++- internal/ui/run_stop.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/stop.go b/cmd/stop.go index 480a46e..33bda33 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -40,6 +40,3 @@ func newStopCmd() *cobra.Command { } } -func init() { - rootCmd.AddCommand(newStopCmd()) -} diff --git a/internal/container/stop.go b/internal/container/stop.go index 7af4319..1191b76 100644 --- a/internal/container/stop.go +++ b/internal/container/stop.go @@ -18,7 +18,10 @@ func Stop(ctx context.Context, rt runtime.Runtime, sink output.Sink) error { for _, c := range cfg.Containers { name := c.Name() running, err := rt.IsRunning(ctx, name) - if err != nil || !running { + if err != nil { + return fmt.Errorf("checking %s running: %w", name, err) + } + if !running { return fmt.Errorf("%s is not running", name) } output.EmitSpinnerStart(sink, fmt.Sprintf("Stopping %s", name)) diff --git a/internal/ui/run_stop.go b/internal/ui/run_stop.go index b917a93..1c4c417 100644 --- a/internal/ui/run_stop.go +++ b/internal/ui/run_stop.go @@ -12,7 +12,7 @@ import ( ) func RunStop(parentCtx context.Context, rt runtime.Runtime) error { - _, cancel := context.WithCancel(parentCtx) + ctx, cancel := context.WithCancel(parentCtx) defer cancel() app := NewApp("", "", "", cancel) @@ -20,7 +20,7 @@ func RunStop(parentCtx context.Context, rt runtime.Runtime) error { runErrCh := make(chan error, 1) go func() { - err := container.Stop(parentCtx, rt, output.NewTUISink(programSender{p: p})) + 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}) From 551355975480d0a802c404f3154846e67c5fee2a Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 4 Mar 2026 21:54:53 +0200 Subject: [PATCH 3/4] Fix test --- internal/runtime/docker.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index 6feb8f6..dddb9af 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -148,6 +148,9 @@ func (d *DockerRuntime) Remove(ctx context.Context, containerName string) error func (d *DockerRuntime) IsRunning(ctx context.Context, containerID string) (bool, error) { inspect, err := d.client.ContainerInspect(ctx, containerID) if err != nil { + if errdefs.IsNotFound(err) { + return false, nil + } return false, err } return inspect.State.Running, nil From 7f3e3cca4f5307027f2dbceae101f3eff089f60b Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 4 Mar 2026 22:00:35 +0200 Subject: [PATCH 4/4] Hide empty header in stop command TUI --- internal/ui/run_stop.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/run_stop.go b/internal/ui/run_stop.go index 1c4c417..b2ba8ce 100644 --- a/internal/ui/run_stop.go +++ b/internal/ui/run_stop.go @@ -15,7 +15,7 @@ func RunStop(parentCtx context.Context, rt runtime.Runtime) error { ctx, cancel := context.WithCancel(parentCtx) defer cancel() - app := NewApp("", "", "", cancel) + app := NewApp("", "", "", cancel, withoutHeader()) p := tea.NewProgram(app, tea.WithInput(os.Stdin), tea.WithOutput(os.Stdout)) runErrCh := make(chan error, 1)