package tui import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "agentu/internal/tools" ) const workflowCommandTimeout = 2 * time.Minute func (m model) handleStatusCommand(args []string) (string, error) { if len(args) != 0 { return "", fmt.Errorf("usage: /status") } out, err := m.runGitCommand("status", "--short") if err != nil { return out, err } if strings.TrimSpace(out) == "" { return "Working tree clean.", nil } return "Changed files:\n" + out, nil } func (m model) handleDiffCommand(args []string) (string, error) { if len(args) > 0 && args[0] == "--stat" { out, err := m.runGitCommand(append([]string{"diff", "--stat", "--"}, args[1:]...)...) if err != nil { return out, err } if strings.TrimSpace(out) == "" { return "No unstaged diff.", nil } return out, nil } out, err := m.runGitCommand(append([]string{"diff", "--"}, args...)...) if err != nil { return out, err } if strings.TrimSpace(out) == "" { return "No unstaged diff.", nil } return out, nil } func (m model) handleTestCommand(args []string) (string, error) { command := strings.TrimSpace(strings.Join(args, " ")) if command == "" { var err error command, err = defaultTestCommand(m.workflowDir()) if err != nil { return "", err } } out, err := m.runShellCommand(command) if err != nil { if strings.TrimSpace(out) != "" { return out, err } return "", err } if strings.TrimSpace(out) == "" { return fmt.Sprintf("$ %s\n(no output)", command), nil } return out, nil } func (m model) runGitCommand(args ...string) (string, error) { ctx, cancel := context.WithTimeout(m.ctx, workflowCommandTimeout) defer cancel() cmd := exec.CommandContext(ctx, "git", append([]string{"-C", m.workflowDir()}, args...)...) output, err := cmd.CombinedOutput() text := tools.FormatResult(string(output)) if errors.Is(ctx.Err(), context.DeadlineExceeded) { return text, fmt.Errorf("git command timed out: %w", ctx.Err()) } if err != nil { if strings.TrimSpace(text) != "" { return text, fmt.Errorf("git command failed: %w", err) } return "", fmt.Errorf("git command failed: %w", err) } return text, nil } func (m model) runShellCommand(command string) (string, error) { ctx, cancel := context.WithTimeout(m.ctx, workflowCommandTimeout) defer cancel() shell := os.Getenv("SHELL") if shell == "" { shell = "/bin/sh" } cmd := exec.CommandContext(ctx, shell, "-lc", command) cmd.Dir = m.workflowDir() var combined bytes.Buffer cmd.Stdout = &combined cmd.Stderr = &combined err := cmd.Run() text := strings.TrimRight(tools.FormatResult(combined.String()), "\n") if text != "" { text = fmt.Sprintf("$ %s\n%s", command, text) } if errors.Is(ctx.Err(), context.DeadlineExceeded) { return text, fmt.Errorf("command timed out: %w", ctx.Err()) } if err != nil { return text, fmt.Errorf("command failed: %w", err) } return text, nil } func (m model) workflowDir() string { if strings.TrimSpace(m.workingDir) == "" { return "." } return m.workingDir } func defaultTestCommand(dir string) (string, error) { if fileExists(filepath.Join(dir, "go.mod")) { return "go test ./...", nil } if fileExists(filepath.Join(dir, "package.json")) { return "npm test", nil } return "", fmt.Errorf("no default test command found; use /test ") } func fileExists(path string) bool { info, err := os.Stat(path) return err == nil && !info.IsDir() }