From 830e7e4270c7814c50276f7b6912123e8b673ecc Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 21:42:23 +0800 Subject: [PATCH] feat: waiter connection manager - save/switch/delete connections, auto-apply default --- cmd/waiter/builtin.go | 46 ++++++++++++++++++ cmd/waiter/config.go | 105 ++++++++++++++++++++++++++++++++++++++++-- cmd/waiter/main.go | 1 + 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/cmd/waiter/builtin.go b/cmd/waiter/builtin.go index 94bae95..b889268 100644 --- a/cmd/waiter/builtin.go +++ b/cmd/waiter/builtin.go @@ -17,6 +17,10 @@ func handleBuiltin(cmd string, cfg *Config, state *State, reconnect func()) bool /connect switch to a different unix socket /remote switch to remote HTTP mode /local switch back to local socket mode + /conn list list saved connections + /conn save save current connection as + /conn use switch to saved connection + /conn del delete saved connection Server commands (sent to agent): /status system status @@ -66,6 +70,48 @@ Any other text is sent to the agent directly.`) reconnect() return true + case cmd == "/conn list": + if len(cfg.Connections) == 0 { + fmt.Println("no saved connections") + } + for _, c := range cfg.Connections { + mark := " " + if c.Name == cfg.Default { + mark = "*" + } + addr := c.Remote + if addr == "" { + addr = c.Socket + } + fmt.Printf(" %s %-15s %s\n", mark, c.Name, addr) + } + return true + + case strings.HasPrefix(cmd, "/conn save "): + name := strings.TrimSpace(cmd[11:]) + cfg.SaveConnection(name) + fmt.Printf("connection saved as '%s' (default)\n", name) + return true + + case strings.HasPrefix(cmd, "/conn use "): + name := strings.TrimSpace(cmd[10:]) + if cfg.SwitchConnection(name) { + fmt.Printf("switched to '%s'\n", name) + reconnect() + } else { + fmt.Printf("connection '%s' not found\n", name) + } + return true + + case strings.HasPrefix(cmd, "/conn del "): + name := strings.TrimSpace(cmd[10:]) + if cfg.DeleteConnection(name) { + fmt.Printf("connection '%s' deleted\n", name) + } else { + fmt.Printf("connection '%s' not found\n", name) + } + return true + case cmd == "/status": if rc := state.RemoteConn(); rc != nil { d, _ := rc.DoAPI("GET", "/api/v1/status", "") diff --git a/cmd/waiter/config.go b/cmd/waiter/config.go index 0f612fa..1dc55e5 100644 --- a/cmd/waiter/config.go +++ b/cmd/waiter/config.go @@ -8,10 +8,40 @@ import ( "gopkg.in/yaml.v3" ) +type Connection struct { + Name string `yaml:"name"` + Socket string `yaml:"socket,omitempty"` + Remote string `yaml:"remote,omitempty"` + APIKey string `yaml:"api_key,omitempty"` +} + type Config struct { - Socket string `yaml:"socket"` - Remote string `yaml:"remote"` - APIKey string `yaml:"api_key"` + Socket string `yaml:"socket"` + Remote string `yaml:"remote"` + APIKey string `yaml:"api_key"` + Default string `yaml:"default"` + Connections []Connection `yaml:"connections,omitempty"` +} + +func (c *Config) Active() *Connection { + for i := range c.Connections { + if c.Connections[i].Name == c.Default { + return &c.Connections[i] + } + } + return nil +} + +func (c *Config) ApplyDefault() { + conn := c.Active() + if conn == nil { + return + } + if c.Socket == "" && c.Remote == "" { + c.Socket = conn.Socket + c.Remote = conn.Remote + c.APIKey = conn.APIKey + } } func discoverConfig(configPath string) *Config { @@ -48,6 +78,14 @@ func configCandidates() []string { return cands } +func configPath() string { + home, _ := os.UserHomeDir() + if home == "" { + return "" + } + return filepath.Join(home, ".config", "homeagent", "waiter.yaml") +} + func readFile(path string) *Config { data, err := os.ReadFile(path) if err != nil { @@ -61,6 +99,19 @@ func readFile(path string) *Config { return &cfg } +func (c *Config) Save() { + p := configPath() + if p == "" { + return + } + os.MkdirAll(filepath.Dir(p), 0755) + data, err := yaml.Marshal(c) + if err != nil { + return + } + os.WriteFile(p, data, 0644) +} + func (c *Config) MergeCLI(socket, remote, apiKey string) { if socket != "" { c.Socket = socket @@ -72,3 +123,51 @@ func (c *Config) MergeCLI(socket, remote, apiKey string) { c.APIKey = apiKey } } + +func (c *Config) SaveConnection(name string) { + conn := Connection{ + Name: name, + Socket: c.Socket, + Remote: c.Remote, + APIKey: c.APIKey, + } + for i, existing := range c.Connections { + if existing.Name == name { + c.Connections[i] = conn + c.Default = name + c.Save() + return + } + } + c.Connections = append(c.Connections, conn) + c.Default = name + c.Save() +} + +func (c *Config) SwitchConnection(name string) bool { + for _, conn := range c.Connections { + if conn.Name == name { + c.Socket = conn.Socket + c.Remote = conn.Remote + c.APIKey = conn.APIKey + c.Default = name + c.Save() + return true + } + } + return false +} + +func (c *Config) DeleteConnection(name string) bool { + for i, conn := range c.Connections { + if conn.Name == name { + c.Connections = append(c.Connections[:i], c.Connections[i+1:]...) + if c.Default == name { + c.Default = "" + } + c.Save() + return true + } + } + return false +} diff --git a/cmd/waiter/main.go b/cmd/waiter/main.go index 9c31a9e..92b80c8 100644 --- a/cmd/waiter/main.go +++ b/cmd/waiter/main.go @@ -86,6 +86,7 @@ func main() { cfg := discoverConfig(*configPath) cfg.MergeCLI(*socket, *remote, *apiKey) + cfg.ApplyDefault() if cfg.Socket == "" && cfg.Remote == "" { cfg.Socket = discoverSocket("")