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
This commit is contained in:
86
waitfor/main.go
Normal file
86
waitfor/main.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user