85 lines
2.2 KiB
Go
85 lines
2.2 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"os/signal"
|
||
|
|
"path/filepath"
|
||
|
|
"syscall"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type options struct {
|
||
|
|
socket string
|
||
|
|
state string
|
||
|
|
base time.Duration
|
||
|
|
cap time.Duration
|
||
|
|
poll time.Duration
|
||
|
|
}
|
||
|
|
|
||
|
|
func statePath() string {
|
||
|
|
root := os.Getenv("XDG_STATE_HOME")
|
||
|
|
if root == "" {
|
||
|
|
home, _ := os.UserHomeDir()
|
||
|
|
root = filepath.Join(home, ".local", "state")
|
||
|
|
}
|
||
|
|
return filepath.Join(root, "codex-keep", "state.json")
|
||
|
|
}
|
||
|
|
|
||
|
|
func socketPath() string {
|
||
|
|
home := os.Getenv("CODEX_HOME")
|
||
|
|
if home == "" {
|
||
|
|
root, _ := os.UserHomeDir()
|
||
|
|
home = filepath.Join(root, ".codex")
|
||
|
|
}
|
||
|
|
return filepath.Join(home, "app-server-control", "app-server-control.sock")
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseOptions(args []string, output io.Writer) (options, error) {
|
||
|
|
var opts options
|
||
|
|
flags := flag.NewFlagSet("codex-keep", flag.ContinueOnError)
|
||
|
|
flags.SetOutput(output)
|
||
|
|
flags.StringVar(&opts.socket, "socket", socketPath(), "Codex daemon Unix socket")
|
||
|
|
flags.StringVar(&opts.state, "state", statePath(), "Recovery checkpoint")
|
||
|
|
flags.DurationVar(&opts.base, "base-delay", 5*time.Second, "Initial retry delay")
|
||
|
|
flags.DurationVar(&opts.cap, "max-delay", 5*time.Minute, "Retry delay ceiling")
|
||
|
|
flags.DurationVar(&opts.poll, "poll", 5*time.Second, "Reconciliation interval")
|
||
|
|
flags.Usage = func() {
|
||
|
|
_, _ = fmt.Fprintln(output, "Usage: codex-keep [options]")
|
||
|
|
_, _ = fmt.Fprintln(output, "Reconcile failed active goals through the shared Codex daemon.")
|
||
|
|
flags.PrintDefaults()
|
||
|
|
}
|
||
|
|
if err := flags.Parse(args); err != nil {
|
||
|
|
return opts, err
|
||
|
|
}
|
||
|
|
if len(flags.Args()) != 0 {
|
||
|
|
return opts, errors.New("unexpected positional arguments")
|
||
|
|
}
|
||
|
|
if opts.base <= 0 || opts.cap < opts.base || opts.poll <= 0 {
|
||
|
|
return opts, errors.New("delays must satisfy 0 < base-delay <= max-delay and poll > 0")
|
||
|
|
}
|
||
|
|
if opts.state == "" {
|
||
|
|
return opts, errors.New("state path is empty")
|
||
|
|
}
|
||
|
|
return opts, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
opts, err := parseOptions(os.Args[1:], os.Stderr)
|
||
|
|
if errors.Is(err, flag.ErrHelp) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err == nil {
|
||
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||
|
|
defer stop()
|
||
|
|
err = run(ctx, opts, os.Stderr)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, "codex-keep:", err)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
}
|