From 08669edb6068ed766791faf6da49267d767c2269 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 4 Mar 2026 02:20:38 +0200 Subject: [PATCH] Collapse start command output --- internal/container/start.go | 26 +++++---- internal/output/events.go | 2 +- internal/output/plain_format.go | 28 ++------- internal/output/plain_format_test.go | 26 ++++----- internal/output/plain_sink_test.go | 86 +++++----------------------- 5 files changed, 45 insertions(+), 123 deletions(-) diff --git a/internal/container/start.go b/internal/container/start.go index 707f38b..904d131 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -94,16 +94,12 @@ func pullImages(ctx context.Context, rt runtime.Runtime, sink output.Sink, conta return fmt.Errorf("failed to remove existing container %s: %w", c.Name, err) } - output.EmitStatus(sink, "pulling", c.Image, "") - progress := make(chan runtime.PullProgress) - go func() { - for p := range progress { - output.EmitProgress(sink, c.Image, p.LayerID, p.Status, p.Current, p.Total) - } - }() - if err := rt.PullImage(ctx, c.Image, progress); err != nil { + output.EmitSpinnerStart(sink, fmt.Sprintf("Pulling %s", c.Image)) + if err := rt.PullImage(ctx, c.Image, nil); err != nil { + output.EmitSpinnerStop(sink) return fmt.Errorf("failed to pull image %s: %w", c.Image, err) } + output.EmitSpinnerStop(sink) } return nil } @@ -119,19 +115,21 @@ func validateLicenses(ctx context.Context, rt runtime.Runtime, sink output.Sink, func startContainers(ctx context.Context, rt runtime.Runtime, sink output.Sink, containers []runtime.ContainerConfig) error { for _, c := range containers { - output.EmitStatus(sink, "starting", c.Name, "") + output.EmitSpinnerStart(sink, "Starting LocalStack") containerID, err := rt.Start(ctx, c) if err != nil { + output.EmitSpinnerStop(sink) return fmt.Errorf("failed to start %s: %w", c.Name, err) } - output.EmitStatus(sink, "waiting", c.Name, "") healthURL := fmt.Sprintf("http://localhost:%s%s", c.Port, c.HealthPath) if err := awaitStartup(ctx, rt, sink, containerID, c.Name, healthURL); err != nil { + output.EmitSpinnerStop(sink) return err } - output.EmitStatus(sink, "ready", c.Name, fmt.Sprintf("containerId: %s", containerID[:12])) + output.EmitSpinnerStop(sink) + output.EmitStatus(sink, "ready", c.Name, "") } return nil } @@ -169,7 +167,7 @@ func validateLicense(ctx context.Context, rt runtime.Runtime, sink output.Sink, version = actualVersion } - output.EmitStatus(sink, "validating license", containerConfig.Name, version) + output.EmitSpinnerStart(sink, "Validating license") hostname, _ := os.Hostname() licenseReq := &api.LicenseRequest{ @@ -188,9 +186,13 @@ func validateLicense(ctx context.Context, rt runtime.Runtime, sink output.Sink, } if err := platformClient.GetLicense(ctx, licenseReq); err != nil { + output.EmitSpinnerStop(sink) return fmt.Errorf("license validation failed for %s:%s: %w", containerConfig.ProductName, version, err) } + output.EmitSpinnerStop(sink) + output.EmitStatus(sink, "valid license", containerConfig.Name, version) + return nil } diff --git a/internal/output/events.go b/internal/output/events.go index 11c58d8..26cbfb3 100644 --- a/internal/output/events.go +++ b/internal/output/events.go @@ -74,7 +74,7 @@ func (f SinkFunc) emit(event any) { } type ContainerStatusEvent struct { - Phase string // e.g., "pulling", "starting", "waiting", "ready" + Phase string // e.g., "valid license", "ready" Container string Detail string // optional extra info (e.g., container ID) } diff --git a/internal/output/plain_format.go b/internal/output/plain_format.go index ae363b6..a3c7f7e 100644 --- a/internal/output/plain_format.go +++ b/internal/output/plain_format.go @@ -22,7 +22,7 @@ func FormatEventLine(event any) (string, bool) { case ContainerStatusEvent: return formatStatusLine(e), true case ProgressEvent: - return formatProgressLine(e) + return "", false case UserInputRequestEvent: return formatUserInputRequest(e), true case ContainerLogLineEvent: @@ -32,19 +32,14 @@ func FormatEventLine(event any) (string, bool) { } } +const successPrefix = "\033[32m✓\033[0m " + func formatStatusLine(e ContainerStatusEvent) string { switch e.Phase { - case "pulling": - return fmt.Sprintf("Pulling %s...", e.Container) - case "starting": - return fmt.Sprintf("Starting %s...", e.Container) - case "waiting": - return fmt.Sprintf("Waiting for %s to be ready...", e.Container) + case "valid license": + return successPrefix + "License activated" case "ready": - if e.Detail != "" { - return fmt.Sprintf("%s ready (%s)", e.Container, e.Detail) - } - return fmt.Sprintf("%s ready", e.Container) + return successPrefix + "LocalStack ready" default: if e.Detail != "" { return fmt.Sprintf("%s: %s (%s)", e.Container, e.Phase, e.Detail) @@ -53,17 +48,6 @@ func formatStatusLine(e ContainerStatusEvent) string { } } -func formatProgressLine(e ProgressEvent) (string, bool) { - if e.Total > 0 { - pct := float64(e.Current) / float64(e.Total) * 100 - return fmt.Sprintf(" %s: %s %.1f%%", e.LayerID, e.Status, pct), true - } - if e.Status != "" { - return fmt.Sprintf(" %s: %s", e.LayerID, e.Status), true - } - return "", false -} - func formatUserInputRequest(e UserInputRequestEvent) string { return FormatPrompt(e.Prompt, e.Options) } diff --git a/internal/output/plain_format_test.go b/internal/output/plain_format_test.go index 3abf1e3..6e5e670 100644 --- a/internal/output/plain_format_test.go +++ b/internal/output/plain_format_test.go @@ -48,31 +48,25 @@ func TestFormatEventLine(t *testing.T) { wantOK: true, }, { - name: "status pulling", - event: ContainerStatusEvent{Phase: "pulling", Container: "localstack/localstack:latest"}, - want: "Pulling localstack/localstack:latest...", + name: "status ready", + event: ContainerStatusEvent{Phase: "ready", Container: "localstack"}, + want: "\033[32m✓\033[0m LocalStack ready", wantOK: true, }, { - name: "status ready with detail", - event: ContainerStatusEvent{Phase: "ready", Container: "localstack", Detail: "abc123"}, - want: "localstack ready (abc123)", - wantOK: true, - }, - { - name: "progress with total", + name: "progress suppressed with total", event: ProgressEvent{LayerID: "abc123", Status: "Downloading", Current: 50, Total: 100}, - want: " abc123: Downloading 50.0%", - wantOK: true, + want: "", + wantOK: false, }, { - name: "progress with status only", + name: "progress suppressed with status only", event: ProgressEvent{LayerID: "abc123", Status: "Pull complete"}, - want: " abc123: Pull complete", - wantOK: true, + want: "", + wantOK: false, }, { - name: "progress ignored when empty", + name: "progress suppressed when empty", event: ProgressEvent{LayerID: "abc123"}, want: "", wantOK: false, diff --git a/internal/output/plain_sink_test.go b/internal/output/plain_sink_test.go index b3bd661..13c988a 100644 --- a/internal/output/plain_sink_test.go +++ b/internal/output/plain_sink_test.go @@ -42,29 +42,9 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) { expected string }{ { - name: "pulling phase", - event: ContainerStatusEvent{Phase: "pulling", Container: "localstack/localstack:latest"}, - expected: "Pulling localstack/localstack:latest...\n", - }, - { - name: "starting phase", - event: ContainerStatusEvent{Phase: "starting", Container: "localstack"}, - expected: "Starting localstack...\n", - }, - { - name: "waiting phase", - event: ContainerStatusEvent{Phase: "waiting", Container: "localstack"}, - expected: "Waiting for localstack to be ready...\n", - }, - { - name: "ready phase with detail", - event: ContainerStatusEvent{Phase: "ready", Container: "localstack", Detail: "abc123"}, - expected: "localstack ready (abc123)\n", - }, - { - name: "ready phase without detail", + name: "ready phase", event: ContainerStatusEvent{Phase: "ready", Container: "localstack"}, - expected: "localstack ready\n", + expected: "\033[32m✓\033[0m LocalStack ready\n", }, { name: "unknown phase with detail", @@ -90,54 +70,19 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) { } } -func TestPlainSink_EmitsProgressEvent(t *testing.T) { - tests := []struct { - name string - event ProgressEvent - expected string - }{ - { - name: "with total (percentage)", - event: ProgressEvent{ - Container: "localstack", - LayerID: "abc123", - Status: "Downloading", - Current: 50, - Total: 100, - }, - expected: " abc123: Downloading 50.0%\n", - }, - { - name: "without total (status only)", - event: ProgressEvent{ - Container: "localstack", - LayerID: "abc123", - Status: "Pull complete", - Current: 0, - Total: 0, - }, - expected: " abc123: Pull complete\n", - }, - { - name: "no status and no total", - event: ProgressEvent{ - Container: "localstack", - LayerID: "abc123", - }, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var out bytes.Buffer - sink := NewPlainSink(&out) +func TestPlainSink_SuppressesProgressEvent(t *testing.T) { + var out bytes.Buffer + sink := NewPlainSink(&out) - Emit(sink, tt.event) + Emit(sink, ProgressEvent{ + Container: "localstack", + LayerID: "abc123", + Status: "Downloading", + Current: 50, + Total: 100, + }) - assert.Equal(t, tt.expected, out.String()) - }) - } + assert.Equal(t, "", out.String()) } func TestPlainSink_EmitsContainerLogLineEvent(t *testing.T) { @@ -222,8 +167,7 @@ func TestPlainSink_UsesFormatterParity(t *testing.T) { AuthEvent{Code: "ABC123", URL: "https://example.com"}, SpinnerEvent{Active: true, Text: "Loading"}, ErrorEvent{Title: "Failed", Summary: "Something broke"}, - ContainerStatusEvent{Phase: "starting", Container: "localstack"}, - ProgressEvent{LayerID: "abc", Status: "Downloading", Current: 1, Total: 2}, + ContainerStatusEvent{Phase: "ready", Container: "localstack"}, } for _, event := range events { @@ -241,8 +185,6 @@ func TestPlainSink_UsesFormatterParity(t *testing.T) { Emit(sink, e) case ContainerStatusEvent: Emit(sink, e) - case ProgressEvent: - Emit(sink, e) default: t.Fatalf("unsupported event type in test: %T", event) }