Files
agent-tools/waitfor/main.go
claude 6c8cae885b waitfor: block until a shell condition holds
--cmd exits 0 (+ optional --matches regex) or --timeout; exit 0/3/1,
last output always printed, --then hook composes with notifyr.
Injectable runner for tests + real-shell integration test.
Spec: doc/tool-parity.md 3.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
2026-08-07 00:39:58 +02:00

87 lines
2.8 KiB
Go

// 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)
}
}
}