helmd v2: Go-server som styr agent i tmux — REST+SSE, frågedetektor, fildelning, ntfy, CI-release
- tmux som sanningskälla (agentoberoende, överlever omstart via adoption) - API: sessions, prompt, question/answer, keys, mode, config, shares, notify, events - frågedetektor testad mot riktiga agy 1.1.9-dumpar + mock - integrationstest: 26 tester i isolerad debian-container (scripts/run-integration.sh) - e2e-verifierad mot riktig agy (trust-fråga -> svar -> prompt) - .gitea/workflows/helmd-release.yaml -> rullande helmd-latest (x64+arm64) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
93
helmd/tmux.go
Normal file
93
helmd/tmux.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tmuxCmd runs a tmux command and returns its stdout.
|
||||
func tmuxCmd(args ...string) (string, error) {
|
||||
out, err := exec.Command("tmux", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tmux %s: %v: %s", strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func tmuxHasSession(name string) bool {
|
||||
err := exec.Command("tmux", "has-session", "-t", "="+name).Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func tmuxListSessions() []string {
|
||||
out, err := tmuxCmd("list-sessions", "-F", "#{session_name}")
|
||||
if err != nil {
|
||||
return nil // no tmux server running = no sessions
|
||||
}
|
||||
var names []string
|
||||
for _, l := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
if l != "" {
|
||||
names = append(names, l)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func tmuxNewSession(name, cwd string, width, height int, cmdline string) error {
|
||||
args := []string{"new-session", "-d", "-s", name,
|
||||
"-x", fmt.Sprint(width), "-y", fmt.Sprint(height)}
|
||||
if cwd != "" {
|
||||
args = append(args, "-c", cwd)
|
||||
}
|
||||
if cmdline != "" {
|
||||
args = append(args, cmdline)
|
||||
}
|
||||
_, err := tmuxCmd(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func tmuxKillSession(name string) error {
|
||||
_, err := tmuxCmd("kill-session", "-t", "="+name)
|
||||
return err
|
||||
}
|
||||
|
||||
// tmuxCapture returns the visible pane content. ansi keeps escape
|
||||
// sequences (for terminal-faithful rendering in clients).
|
||||
func tmuxCapture(name string, ansi bool, history int) (string, error) {
|
||||
args := []string{"capture-pane", "-p", "-t", "=" + name + ":"}
|
||||
if ansi {
|
||||
args = append(args, "-e")
|
||||
}
|
||||
if history > 0 {
|
||||
args = append(args, "-S", fmt.Sprintf("-%d", history))
|
||||
}
|
||||
return tmuxCmd(args...)
|
||||
}
|
||||
|
||||
// tmuxSendKeys sends key names (Enter, Up, Down, BTab, C-c, ...).
|
||||
func tmuxSendKeys(name string, keys ...string) error {
|
||||
args := append([]string{"send-keys", "-t", "=" + name + ":"}, keys...)
|
||||
_, err := tmuxCmd(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// tmuxPasteText inserts literal text (multi-line safe) via a paste
|
||||
// buffer, without submitting it.
|
||||
func tmuxPasteText(name, text string) error {
|
||||
cmd := exec.Command("tmux", "load-buffer", "-b", "helmd", "-")
|
||||
cmd.Stdin = strings.NewReader(text)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("tmux load-buffer: %v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
_, err := tmuxCmd("paste-buffer", "-d", "-b", "helmd", "-t", "="+name+":")
|
||||
return err
|
||||
}
|
||||
|
||||
func tmuxSessionAlive(name string) bool {
|
||||
out, err := tmuxCmd("list-panes", "-t", "="+name+":", "-F", "#{pane_dead}")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(out) == "0"
|
||||
}
|
||||
Reference in New Issue
Block a user