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
8 changes: 4 additions & 4 deletions docs/AGENT_SETUP_IMPROVEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ Replace raw `fmt.Println` calls with a structured, visually clear output format

### 2.2 Apply formatter to setup execution

- [ ] Update `Executor.Execute` in `internal/setup/executor.go` to use the formatter instead of raw `fmt.Printf`
- [ ] Show per-command durations on the same line as the step name
- [ ] On failure, show the failing command's stderr indented beneath the step
- [ ] Show skipped phases (phases that never ran due to early failure) at the bottom
- [x] Update `Executor.Execute` in `internal/setup/executor.go` to use the formatter instead of raw `fmt.Printf`
- [x] Show per-command durations on the same line as the step name
- [x] On failure, show the failing command's stderr indented beneath the step
- [x] Show skipped phases (phases that never ran due to early failure) at the bottom

### 2.3 Apply formatter to agent mode

Expand Down
90 changes: 74 additions & 16 deletions internal/setup/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,42 @@ package setup
import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/InitiatDev/initiat-cli/internal/output"
)

type Executor struct {
secrets map[string]string
commandExecutor CommandExecutor
formatter *output.Formatter
}

func NewExecutor(secrets map[string]string) *Executor {
return &Executor{
secrets: secrets,
commandExecutor: NewRealCommandExecutor(),
formatter: output.NewFormatter(os.Stdout),
}
}

func NewExecutorWithCommandExecutor(secrets map[string]string, executor CommandExecutor) *Executor {
return &Executor{
secrets: secrets,
commandExecutor: executor,
formatter: output.NewFormatter(os.Stdout),
}
}

func NewExecutorWithFormatter(
secrets map[string]string, executor CommandExecutor, formatter *output.Formatter,
) *Executor {
return &Executor{
secrets: secrets,
commandExecutor: executor,
formatter: formatter,
}
}

Expand All @@ -33,39 +49,88 @@ func (e *Executor) Execute(plan *ExecutionPlan) error {
Commands: []CommandExecutionRecord{},
}

fmt.Printf("🚀 Executing setup script: %d phases, %d steps, %d commands\n\n",
len(plan.Summary.Phases), plan.Summary.TotalSteps, plan.Summary.TotalCommands)
f := e.formatter
currentPhase := ""
completedPhases := map[string]bool{}

for _, cmd := range plan.Commands {
fmt.Printf("[%s] %s", cmd.Phase, cmd.StepName)
if cmd.Description != "" {
fmt.Printf(": %s", cmd.Description)
if cmd.Phase != currentPhase {
if currentPhase != "" {
f.PhaseEnd()
}
currentPhase = cmd.Phase
f.PhaseStart(currentPhase)
}
fmt.Println()

record, err := e.executeCommandWithReport(cmd)
report.Commands = append(report.Commands, record)

duration := lastAttemptDuration(record)
stepLabel := cmd.StepName
if stepLabel == "" {
stepLabel = cmd.Description
}

if err != nil {
stderr := lastAttemptStderr(record)
f.StepFailure(stepLabel, duration, stderr)

if cmd.ContinueOnError {
fmt.Printf("⚠️ Command failed but continuing: %v\n", err)
continue
}

f.PhaseEnd()
completedPhases[currentPhase] = true

skipped := skippedPhaseNames(plan.Summary.Phases, completedPhases)
if len(skipped) > 0 {
f.PhasesSkipped(skipped)
}

report.FinishedAt = time.Now()
return &SetupExecutionError{
Report: report,
FailedCommand: record,
Err: fmt.Errorf("command failed: %w", err),
}
}

f.StepSuccess(stepLabel, duration)
completedPhases[cmd.Phase] = true
}

if currentPhase != "" {
f.PhaseEnd()
}

fmt.Println()
fmt.Println("✅ Setup script completed successfully!")
report.FinishedAt = time.Now()
return nil
}

func lastAttemptDuration(record CommandExecutionRecord) time.Duration {
if len(record.Attempts) == 0 {
return 0
}
return record.Attempts[len(record.Attempts)-1].Duration
}

func lastAttemptStderr(record CommandExecutionRecord) string {
if len(record.Attempts) == 0 {
return ""
}
return record.Attempts[len(record.Attempts)-1].Stderr
}

func skippedPhaseNames(phases []PhaseSummary, completed map[string]bool) []string {
var skipped []string
for _, p := range phases {
if !completed[p.Name] {
skipped = append(skipped, p.Name)
}
}
return skipped
}

func (e *Executor) executeCommandWithReport(cmd ExecutableCommand) (CommandExecutionRecord, error) {
record := CommandExecutionRecord{
Phase: cmd.Phase,
Expand All @@ -89,7 +154,6 @@ func (e *Executor) executeCommandWithReport(cmd ExecutableCommand) (CommandExecu

for attempt := 1; attempt <= attempts; attempt++ {
if attempt > 1 {
fmt.Printf(" ↻ Retry attempt %d/%d...\n", attempt, attempts)
if cmd.Retries != nil && cmd.Retries.Backoff > 0 {
time.Sleep(cmd.Retries.Backoff)
}
Expand All @@ -99,17 +163,11 @@ func (e *Executor) executeCommandWithReport(cmd ExecutableCommand) (CommandExecu
record.Attempts = append(record.Attempts, e.toAttemptRecord(attempt, res, err))

if err == nil {
if attempt > 1 {
fmt.Printf(" ✅ Command succeeded on retry\n")
}
record.Success = true
return record, nil
}

lastErr = err
if attempt < attempts {
fmt.Printf(" ⚠️ Command failed, will retry: %v\n", err)
}
}

record.Success = false
Expand Down
Loading
Loading