Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions internal/command/cli_progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"fmt"
"os"

tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
Expand All @@ -21,8 +22,10 @@ type cliProgress struct {
task func(docker.DeployProgressCallback) error
}

type cliProgressDoneMsg struct{ err error }
type cliProgressUpdateMsg struct{ p docker.DeployProgress }
type (
cliProgressDoneMsg struct{ err error }
cliProgressUpdateMsg struct{ p docker.DeployProgress }
)

func newCLIProgress(label string, task func(docker.DeployProgressCallback) error) *cliProgress {
return &cliProgress{
Expand All @@ -34,19 +37,26 @@ func newCLIProgress(label string, task func(docker.DeployProgressCallback) error
}
}

func (m *cliProgress) Run() error {
_, err := tea.NewProgram(m).Run()
if err != nil {
return err
func runWithProgress(label string, task func(docker.DeployProgressCallback) error) error {
var err error

if isTerminal() {
p := newCLIProgress(label, task)
if _, runErr := tea.NewProgram(p).Run(); runErr != nil {
return runErr
}
err = p.err
} else {
err = task(func(docker.DeployProgress) {})
}

if m.err != nil {
fmt.Printf("%s: %s\n", m.label, lipgloss.NewStyle().Foreground(lipgloss.Red).Render("failed"))
if err != nil {
fmt.Printf("%s: %s\n", label, lipgloss.NewStyle().Foreground(lipgloss.Red).Render("failed"))
} else {
fmt.Printf("%s: %s\n", m.label, lipgloss.NewStyle().Foreground(lipgloss.Green).Render("done"))
fmt.Printf("%s: %s\n", label, lipgloss.NewStyle().Foreground(lipgloss.Green).Render("done"))
}

return m.err
return err
}

func (m *cliProgress) Init() tea.Cmd {
Expand Down Expand Up @@ -117,3 +127,13 @@ func (m *cliProgress) waitForProgress() tea.Cmd {
return cliProgressUpdateMsg{p: p}
}
}

// Helpers

func isTerminal() bool {
fi, err := os.Stdout.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice != 0
Comment on lines +133 to +138
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

isTerminal() only checks whether stdout is a character device. That can return true even when there’s no usable TTY for interactive Bubble Tea input (e.g., stdin is redirected), so progress UI may still be started in non-interactive contexts.

Consider using the existing github.com/charmbracelet/x/term dependency and requiring both stdin and stdout to be terminals (or checking for a controlling TTY via /dev/tty where applicable) before enabling the TUI.

Copilot uses AI. Check for mistakes.
}
5 changes: 1 addition & 4 deletions internal/command/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ func (d *deployCommand) run(ctx context.Context, ns *docker.Namespace, cmd *cobr
}

settings, err := d.flags.buildSettings(imageRef, host)

if err != nil {
return err
}
Expand All @@ -62,7 +61,7 @@ func (d *deployCommand) run(ctx context.Context, ns *docker.Namespace, cmd *cobr

app := docker.NewApplication(ns, settings)

p := newCLIProgress("Deploying "+host, func(progress docker.DeployProgressCallback) error {
return runWithProgress("Deploying "+host, func(progress docker.DeployProgressCallback) error {
if err := app.Deploy(ctx, progress); err != nil {
if cleanupErr := app.Destroy(context.Background(), true); cleanupErr != nil {
slog.Error("Failed to clean up after deploy failure", "app", name, "error", cleanupErr)
Expand All @@ -76,6 +75,4 @@ func (d *deployCommand) run(ctx context.Context, ns *docker.Namespace, cmd *cobr

return nil
})

return p.Run()
}
4 changes: 1 addition & 3 deletions internal/command/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,11 @@ func (u *updateCommand) run(ctx context.Context, ns *docker.Namespace, cmd *cobr
oldSettings := app.Settings
app.Settings = settings

p := newCLIProgress("Updating "+currentHost, func(progress docker.DeployProgressCallback) error {
return runWithProgress("Updating "+currentHost, func(progress docker.DeployProgressCallback) error {
if err := app.Deploy(ctx, progress); err != nil {
app.Settings = oldSettings
return fmt.Errorf("%w: %w", docker.ErrDeployFailed, err)
}
return nil
})

return p.Run()
}
Loading