summaryrefslogtreecommitdiff
path: root/cmd/internal/hexai-action/main.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-07 11:36:38 +0300
committerPaul Buetow <paul@buetow.org>2025-09-07 11:36:38 +0300
commitfb06bfa9dc7140f987bdbad59467a84610221fbb (patch)
treeb8ebf92c121a32eed0dc5e193c5877c5d0b94e6b /cmd/internal/hexai-action/main.go
parent0d424adfc64da1c61296c66a99162ec68cc4f8d0 (diff)
refactor: move hexai-action to cmd/hexai-action; extract orchestration into internal/hexaiaction; move tests; update Magefile and docs
Diffstat (limited to 'cmd/internal/hexai-action/main.go')
-rw-r--r--cmd/internal/hexai-action/main.go238
1 files changed, 0 insertions, 238 deletions
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
-}