agent-orchestrator/backend/internal/cli/root.go

137 lines
3.1 KiB
Go

// Package cli implements the user-facing ao command. It stays thin: commands
// discover the local daemon, call its loopback HTTP API, and format output.
package cli
import (
"io"
"net/http"
"os"
"os/exec"
"time"
"github.com/spf13/cobra"
"github.com/aoagents/agent-orchestrator/backend/internal/daemon"
)
// Execute runs the ao CLI with process stdio.
func Execute() error {
return NewRootCommand(DefaultDeps()).Execute()
}
// Deps holds the small set of side effects the CLI needs. Tests replace these
// functions without reaching into process-global state.
type Deps struct {
In io.Reader
Out io.Writer
Err io.Writer
HTTPClient *http.Client
Executable func() (string, error)
StartProcess func(processStartConfig) (processHandle, error)
SignalTerm func(pid int) error
ProcessAlive func(pid int) bool
LookPath func(file string) (string, error)
Now func() time.Time
Sleep func(time.Duration)
}
// DefaultDeps returns production dependencies.
func DefaultDeps() Deps {
return Deps{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
HTTPClient: &http.Client{Timeout: 2 * time.Second},
Executable: os.Executable,
StartProcess: startProcess,
SignalTerm: signalTerm,
ProcessAlive: processAlive,
LookPath: exec.LookPath,
Now: time.Now,
Sleep: time.Sleep,
}
}
func (d Deps) withDefaults() Deps {
def := DefaultDeps()
if d.In == nil {
d.In = def.In
}
if d.Out == nil {
d.Out = def.Out
}
if d.Err == nil {
d.Err = def.Err
}
if d.HTTPClient == nil {
d.HTTPClient = def.HTTPClient
}
if d.Executable == nil {
d.Executable = def.Executable
}
if d.StartProcess == nil {
d.StartProcess = def.StartProcess
}
if d.SignalTerm == nil {
d.SignalTerm = def.SignalTerm
}
if d.ProcessAlive == nil {
d.ProcessAlive = def.ProcessAlive
}
if d.LookPath == nil {
d.LookPath = def.LookPath
}
if d.Now == nil {
d.Now = def.Now
}
if d.Sleep == nil {
d.Sleep = def.Sleep
}
return d
}
// NewRootCommand builds a testable root command.
func NewRootCommand(deps Deps) *cobra.Command {
deps = deps.withDefaults()
ctx := &commandContext{deps: deps}
root := &cobra.Command{
Use: "ao",
Short: "Agent Orchestrator",
Long: "Agent Orchestrator manages the local daemon that supervises parallel coding-agent sessions.",
Version: VersionString(),
SilenceUsage: true,
SilenceErrors: true,
}
root.SetIn(deps.In)
root.SetOut(deps.Out)
root.SetErr(deps.Err)
root.CompletionOptions.DisableDefaultCmd = true
root.AddCommand(newDaemonCommand())
root.AddCommand(newStartCommand(ctx))
root.AddCommand(newStopCommand(ctx))
root.AddCommand(newStatusCommand(ctx))
root.AddCommand(newDoctorCommand(ctx))
root.AddCommand(newCompletionCommand())
root.AddCommand(newVersionCommand())
return root
}
type commandContext struct {
deps Deps
}
func newDaemonCommand() *cobra.Command {
return &cobra.Command{
Use: "daemon",
Short: "Run the AO backend daemon",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return daemon.Run()
},
}
}