// waitfor blocks until a shell condition holds — the agent-friendly // replacement for hand-rolled poll loops that each need a fresh // command approval. See doc/tool-parity.md §3.2. package main import ( "flag" "fmt" "os" "os/exec" "strings" "time" "gitea.brasse-pc.eu/brasse/agent-tools/waitfor/wait" ) var version = "dev" const usage = `waitfor - block until a condition holds Usage: waitfor --cmd "shell command" [--matches regex] [--interval 30s] [--timeout 20m] [--then "shell command"] [--verbose] waitfor version The condition holds when --cmd exits 0 and (if given) its combined output matches --matches. The first attempt runs immediately. Exit codes: 0 condition met, 3 timeout, 1 error. The last command output is printed either way, so the caller always sees the state. Examples: waitfor --cmd "curl -sf https://gitea.brasse-pc.eu/api/healthz" --interval 30s --timeout 20m waitfor --cmd "ssh pi5-claude sudo claude-docker ps" --matches gitea --timeout 10m waitfor --cmd "test -f /tmp/done" --then 'notifyr send --msg "done!"' ` func main() { if len(os.Args) >= 2 && (os.Args[1] == "version" || os.Args[1] == "--version" || os.Args[1] == "-v") { fmt.Println("waitfor", version) return } if len(os.Args) >= 2 && (os.Args[1] == "help" || os.Args[1] == "--help" || os.Args[1] == "-h") { fmt.Print(usage) return } fs := flag.NewFlagSet("waitfor", flag.ExitOnError) cmd := fs.String("cmd", "", "shell command to poll (required)") matches := fs.String("matches", "", "regex the output must match") interval := fs.Duration("interval", 10*time.Second, "time between attempts") timeout := fs.Duration("timeout", 10*time.Minute, "total time budget") then := fs.String("then", "", "shell command to run when the condition is met") verbose := fs.Bool("verbose", false, "log every attempt to stderr") fs.Usage = func() { fmt.Fprint(os.Stderr, usage) } fs.Parse(os.Args[1:]) if *cmd == "" { fmt.Fprint(os.Stderr, usage) os.Exit(1) } res, err := wait.Wait(wait.Options{ Cmd: *cmd, Matches: *matches, Interval: *interval, Timeout: *timeout, Verbose: *verbose, }, wait.ShellRunner, func(s string) { fmt.Fprintln(os.Stderr, "waitfor: "+s) }) if err != nil { fmt.Fprintln(os.Stderr, "waitfor:", err) os.Exit(1) } if out := strings.TrimRight(res.LastOut, "\n"); out != "" { fmt.Println(out) } if !res.Met { fmt.Fprintf(os.Stderr, "waitfor: timeout after %s (%d attempts, last exit %d)\n", res.Elapsed.Round(time.Second), res.Attempts, res.LastExit) os.Exit(3) } fmt.Fprintf(os.Stderr, "waitfor: condition met after %s (%d attempts)\n", res.Elapsed.Round(time.Millisecond), res.Attempts) if *then != "" { t := exec.Command("sh", "-c", *then) t.Stdout, t.Stderr = os.Stdout, os.Stderr if err := t.Run(); err != nil { fmt.Fprintf(os.Stderr, "waitfor: --then command failed: %v\n", err) } } }