From fb06bfa9dc7140f987bdbad59467a84610221fbb Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 7 Sep 2025 11:36:38 +0300 Subject: refactor: move hexai-action to cmd/hexai-action; extract orchestration into internal/hexaiaction; move tests; update Magefile and docs --- cmd/internal/hexai-action/main.go | 238 -------------------------------------- 1 file changed, 238 deletions(-) delete mode 100644 cmd/internal/hexai-action/main.go (limited to 'cmd/internal/hexai-action/main.go') diff --git a/cmd/internal/hexai-action/main.go b/cmd/internal/hexai-action/main.go deleted file mode 100644 index b8ba524..0000000 --- a/cmd/internal/hexai-action/main.go +++ /dev/null @@ -1,238 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "codeberg.org/snonux/hexai/internal/hexaiaction" - "codeberg.org/snonux/hexai/internal/tmux" - "golang.org/x/term" -) - -func main() { - infile := flag.String("infile", "", "Read input from this file instead of stdin") - outfile := flag.String("outfile", "", "Write output to this file instead of stdout") - // Tmux/UI flags - forceTmux := flag.Bool("tmux", false, "Force running the UI in a tmux split-pane (auto if not set)") - noTmux := flag.Bool("no-tmux", false, "Disable tmux mode even if available") - uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") - tmuxTarget := flag.String("tmux-target", "", "tmux split target (advanced)") - tmuxSplit := flag.String("tmux-split", "v", "tmux split orientation: v or h") - tmuxPercent := flag.Int("tmux-percent", 33, "tmux split size percentage (1-100)") - flag.Parse() - - // Child mode: run TUI and write atomically to -outfile - if *uiChild { - if err := runChild(*infile, *outfile); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return - } - - // Parent mode: decide inline vs tmux - if shouldRunInTmux(*forceTmux, *noTmux) { - if err := runInTmuxParent(*tmuxTarget, *tmuxSplit, *tmuxPercent); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return - } - - // Inline path: only if we have a TTY for UI; otherwise echo input - if isTTY(os.Stdout.Fd()) && isTTY(os.Stdin.Fd()) { - in, out, closeIn, closeOut, err := openIO(*infile, *outfile) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - defer closeIn() - defer closeOut() - if err := hexaiactionRun(context.Background(), in, out, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return - } - - // Fallback: no TTY and tmux not available; echo input to output - if err := echoThrough(*infile, *outfile); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -// openIO returns readers/writers for infile/outfile flags with deferred closers. -func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error) { - in := io.Reader(os.Stdin) - out := io.Writer(os.Stdout) - closeIn := func() {} - closeOut := func() {} - - if path := infile; path != "" { - f, err := os.Open(path) - if err != nil { - return nil, nil, func() {}, func() {}, fmt.Errorf("hexai-action: cannot open infile: %w", err) - } - in = f - closeIn = func() { _ = f.Close() } - } - if path := outfile; path != "" { - f, err := os.Create(path) - if err != nil { - return nil, nil, func() {}, func() {}, fmt.Errorf("hexai-action: cannot open outfile: %w", err) - } - out = f - closeOut = func() { _ = f.Close() } - } - return in, out, closeIn, closeOut, nil -} - -// runChild runs the interactive flow and writes the final output atomically to outfile. -var hexaiactionRun = hexaiaction.Run - -func runChild(infile, outfile string) error { - if outfile == "" { - // No atomic handoff needed; just run normally to stdout - in, out, closeIn, closeOut, err := openIO(infile, "") - if err != nil { - return err - } - defer closeIn() - defer closeOut() - return hexaiactionRun(context.Background(), in, out, os.Stderr) - } - tmp := outfile + ".tmp" - in, out, closeIn, closeOut, err := openIO(infile, tmp) - if err != nil { - return err - } - defer closeIn() - if err := hexaiactionRun(context.Background(), in, out, os.Stderr); err != nil { - // On error, try to echo input to tmp to avoid blocking - closeOut() - if copyErr := echoThrough(infile, tmp); copyErr != nil { - return fmt.Errorf("hexai-action child: %v; echo failed: %v", err, copyErr) - } - } else { - closeOut() - } - return os.Rename(tmp, outfile) -} - -var isTTYFn = isTTY -var tmuxAvailableFn = tmux.Available -var splitRunFn = tmux.SplitRun -var osExecutableFn = os.Executable - -func shouldRunInTmux(forceTmux, noTmux bool) bool { - if noTmux { - return false - } - if forceTmux { - return true - } - // Auto: prefer tmux when stdio are not TTYs (Helix :pipe scenario) - if !(isTTYFn(os.Stdin.Fd()) && isTTYFn(os.Stdout.Fd())) && tmuxAvailableFn() { - return true - } - return false -} - -func isTTY(fd uintptr) bool { return term.IsTerminal(int(fd)) } - -func runInTmuxParent(target, split string, percent int) error { - // Prepare temp files - dir, err := os.MkdirTemp("", "hexai-action-") - if err != nil { - return err - } - defer func() { _ = os.RemoveAll(dir) }() - inPath := filepath.Join(dir, "input.txt") - outPath := filepath.Join(dir, "reply.txt") - // Read stdin and persist to inPath - if err := persistStdin(inPath); err != nil { - return err - } - // Build child argv - exe, err := osExecutableFn() - if err != nil { - return err - } - argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath} - // Spawn tmux split - opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent} - if err := splitRunFn(opts, argv); err != nil { - return err - } - // Wait for outfile to appear - if err := waitForFile(outPath, 60*time.Second); err != nil { - return err - } - // Print to stdout - return catFileToStdout(outPath) -} - -func persistStdin(path string) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - if _, err := io.Copy(f, os.Stdin); err != nil { - return err - } - return f.Sync() -} - -func waitForFile(path string, timeout time.Duration) error { - deadline := time.Now().Add(timeout) - for { - if _, err := os.Stat(path); err == nil { - return nil - } - if time.Now().After(deadline) { - return fmt.Errorf("hexai-action: timeout waiting for reply file") - } - time.Sleep(200 * time.Millisecond) - } -} - -func catFileToStdout(path string) error { - f, err := os.Open(path) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - _, err = io.Copy(os.Stdout, f) - return err -} - -func echoThrough(infile, outfile string) error { - // Read from infile or stdin and write to outfile or stdout - var in io.Reader = os.Stdin - var out io.Writer = os.Stdout - if infile != "" { - f, err := os.Open(infile) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - in = f - } - if outfile != "" { - f, err := os.Create(outfile) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - out = f - } - _, err := io.Copy(out, in) - return err -} -- cgit v1.2.3