package system import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" ) type Preflight struct { problems []string } func NewPreflight() *Preflight { return &Preflight{} } func (p *Preflight) RequireCommand(name, hint string) { value := strings.TrimSpace(name) if value == "" { p.add("command name is not configured%s", formatHint(hint)) return } if _, err := exec.LookPath(value); err != nil { p.add("required command %q not found%s", value, formatHint(hint)) } } func (p *Preflight) RequireExecutable(pathOrName, label, hint string) { value := strings.TrimSpace(pathOrName) if value == "" { p.add("%s is not configured%s", label, formatHint(hint)) return } if strings.ContainsRune(value, filepath.Separator) { info, err := os.Stat(value) if err != nil { p.add("%s not found at %s%s", label, value, formatHint(hint)) return } if info.IsDir() || info.Mode()&0o111 == 0 { p.add("%s is not executable at %s%s", label, value, formatHint(hint)) } return } if _, err := exec.LookPath(value); err != nil { p.add("missing %s %q%s", label, value, formatHint(hint)) } } func (p *Preflight) RequireFile(path, label, hint string) { value := strings.TrimSpace(path) if value == "" { p.add("%s is not configured%s", label, formatHint(hint)) return } info, err := os.Stat(value) if err != nil { p.add("%s not found at %s%s", label, value, formatHint(hint)) return } if info.IsDir() { p.add("%s expected a file at %s%s", label, value, formatHint(hint)) } } func (p *Preflight) RequireDir(path, label, hint string) { value := strings.TrimSpace(path) if value == "" { p.add("%s is not configured%s", label, formatHint(hint)) return } info, err := os.Stat(value) if err != nil { p.add("%s not found at %s%s", label, value, formatHint(hint)) return } if !info.IsDir() { p.add("%s expected a directory at %s%s", label, value, formatHint(hint)) } } func (p *Preflight) Addf(format string, args ...any) { p.add(format, args...) } func (p *Preflight) Problems() []string { if len(p.problems) == 0 { return nil } out := make([]string, len(p.problems)) copy(out, p.problems) return out } func (p *Preflight) Err(prefix string) error { if len(p.problems) == 0 { return nil } var builder strings.Builder builder.WriteString(strings.TrimSpace(prefix)) for _, problem := range p.problems { builder.WriteString("\n- ") builder.WriteString(problem) } return errors.New(builder.String()) } func (p *Preflight) add(format string, args ...any) { p.problems = append(p.problems, fmt.Sprintf(format, args...)) } func formatHint(hint string) string { hint = strings.TrimSpace(hint) if hint == "" { return "" } return " (" + hint + ")" }