--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
87 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|