feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
// Package tui is the impure shell layer for the TUI.
|
||||
// It converts pure Intent values into real I/O via tea.Cmd.
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
puretui "github.com/enmanuel/agents/pkg/tui"
|
||||
"github.com/enmanuel/agents/shell/process"
|
||||
)
|
||||
|
||||
// Adapter bridges pure Intents with the process Manager.
|
||||
type Adapter struct {
|
||||
mgr *process.Manager
|
||||
}
|
||||
|
||||
// NewAdapter creates an Adapter with the given Manager.
|
||||
func NewAdapter(mgr *process.Manager) *Adapter {
|
||||
return &Adapter{mgr: mgr}
|
||||
}
|
||||
|
||||
// RunIntent converts a pure Intent into a bubbletea Cmd that performs I/O.
|
||||
func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
||||
switch intent.Kind {
|
||||
|
||||
case puretui.IntentLoadAgents:
|
||||
return a.loadAgents()
|
||||
|
||||
case puretui.IntentEnableAgent:
|
||||
return a.enableAgent(intent.AgentID)
|
||||
|
||||
case puretui.IntentDisableAgent:
|
||||
return a.disableAgent(intent.AgentID)
|
||||
|
||||
case puretui.IntentReloadAgent:
|
||||
return a.reloadAgent(intent.AgentID)
|
||||
|
||||
case puretui.IntentReloadAll:
|
||||
return a.reloadAll()
|
||||
|
||||
case puretui.IntentRestartAgent:
|
||||
return a.restartAgent(intent.AgentID)
|
||||
|
||||
case puretui.IntentLoadLogs:
|
||||
return a.loadLogs(intent.AgentID)
|
||||
|
||||
case puretui.IntentStartLauncher:
|
||||
return a.startLauncher()
|
||||
|
||||
case puretui.IntentStopLauncher:
|
||||
return a.stopLauncher()
|
||||
|
||||
case puretui.IntentRestartLauncher:
|
||||
return a.restartLauncher()
|
||||
|
||||
case puretui.IntentKillLauncher:
|
||||
return a.killLauncher()
|
||||
|
||||
case puretui.IntentRebuildRestart:
|
||||
return a.rebuildRestart()
|
||||
|
||||
case puretui.IntentRunTests:
|
||||
return a.runGoTests()
|
||||
|
||||
case puretui.IntentRunGoTests:
|
||||
return a.runGoTests()
|
||||
|
||||
case puretui.IntentRunE2ETests:
|
||||
return a.runE2ETests(false)
|
||||
|
||||
case puretui.IntentRunE2EHeadTests:
|
||||
return a.runE2ETests(true)
|
||||
|
||||
case puretui.IntentRunAllTests:
|
||||
return a.runAllTests()
|
||||
|
||||
case puretui.IntentTick:
|
||||
return a.tick()
|
||||
|
||||
case puretui.IntentQuit:
|
||||
return tea.Quit
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) loadAgents() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
statuses, err := a.mgr.StatusAllUnified()
|
||||
if err != nil {
|
||||
return puretui.MsgAgentsLoaded{}
|
||||
}
|
||||
|
||||
views := make([]puretui.AgentView, len(statuses))
|
||||
for i, s := range statuses {
|
||||
views[i] = puretui.AgentView{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Version: s.Version,
|
||||
Desc: s.Desc,
|
||||
Enabled: s.Enabled,
|
||||
Running: s.Running,
|
||||
PID: s.PID,
|
||||
}
|
||||
}
|
||||
|
||||
msg := puretui.MsgAgentsLoaded{
|
||||
Agents: views,
|
||||
LauncherRunning: a.mgr.IsUnifiedRunning(),
|
||||
LauncherPID: a.mgr.UnifiedPID(),
|
||||
}
|
||||
|
||||
// Launcher stats
|
||||
if msg.LauncherRunning {
|
||||
if stats, err := a.mgr.UnifiedStats(); err == nil {
|
||||
msg.LauncherUptime = formatUptime(stats.UptimeSecs)
|
||||
msg.LauncherMemory = formatBytes(stats.MemRSSKB * 1024)
|
||||
msg.LauncherCPU = fmt.Sprintf("%.1f%%", stats.CPUPct)
|
||||
msg.LauncherLogSize = formatBytes(stats.LogBytes)
|
||||
}
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) enableAgent(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := a.mgr.ToggleEnabled(id, true)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Enable", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) disableAgent(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := a.mgr.ToggleEnabled(id, false)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Disable", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// reloadAgent hot-reloads a single agent via SIGHUP without stopping the launcher.
|
||||
func (a *Adapter) reloadAgent(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
pid := a.mgr.UnifiedPID()
|
||||
if pid <= 0 {
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Reload",
|
||||
Err: fmt.Errorf("el launcher no está corriendo")}
|
||||
}
|
||||
if id != "" {
|
||||
if err := os.WriteFile("run/reload.txt", []byte(id), 0o644); err != nil {
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Reload", Err: err}
|
||||
}
|
||||
}
|
||||
err := syscall.Kill(pid, syscall.SIGHUP)
|
||||
if err != nil {
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Reload", Err: err}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Reload", Err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
// reloadAll hot-reloads all agents via SIGHUP (no reload.txt → reload all).
|
||||
func (a *Adapter) reloadAll() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
pid := a.mgr.UnifiedPID()
|
||||
if pid <= 0 {
|
||||
return puretui.MsgServerActionDone{Action: "Reload All",
|
||||
Err: fmt.Errorf("el launcher no está corriendo")}
|
||||
}
|
||||
// Remove stale reload.txt so the launcher reloads all agents.
|
||||
_ = os.Remove("run/reload.txt")
|
||||
err := syscall.Kill(pid, syscall.SIGHUP)
|
||||
if err != nil {
|
||||
return puretui.MsgServerActionDone{Action: "Reload All", Err: err}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
return puretui.MsgServerActionDone{Action: "Reload All", Err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
// restartAgent stops and restarts the whole launcher (full restart, all agents).
|
||||
func (a *Adapter) restartAgent(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
_ = a.mgr.StopUnified()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
err := a.mgr.StartUnified()
|
||||
if err == nil {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) startLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := a.mgr.StartUnified()
|
||||
if err == nil {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return puretui.MsgServerActionDone{Action: "Start", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) stopLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := a.mgr.StopUnified()
|
||||
return puretui.MsgServerActionDone{Action: "Stop", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) restartLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
_ = a.mgr.StopUnified()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
err := a.mgr.StartUnified()
|
||||
if err == nil {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return puretui.MsgServerActionDone{Action: "Restart", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) killLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := a.mgr.KillUnified()
|
||||
return puretui.MsgServerActionDone{Action: "Kill", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) rebuildRestart() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
wasRunning := a.mgr.IsUnifiedRunning()
|
||||
|
||||
// Stop if running
|
||||
if wasRunning {
|
||||
_ = a.mgr.StopUnified()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Build
|
||||
buildOut, buildErr := a.mgr.Build()
|
||||
if buildErr != nil {
|
||||
// Build failed — try to restart if was running
|
||||
if wasRunning {
|
||||
_ = a.mgr.StartUnified()
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(buildOut), "\n")
|
||||
tail := buildOut
|
||||
if len(lines) > 5 {
|
||||
tail = strings.Join(lines[len(lines)-5:], "\n")
|
||||
}
|
||||
return puretui.MsgRebuildDone{BuildOK: false, BuildLog: tail}
|
||||
}
|
||||
|
||||
// Restart launcher
|
||||
started := false
|
||||
var startErr error
|
||||
if wasRunning {
|
||||
startErr = a.mgr.StartUnified()
|
||||
if startErr == nil {
|
||||
started = true
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return puretui.MsgRebuildDone{
|
||||
BuildOK: true,
|
||||
Started: started,
|
||||
Err: startErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) loadLogs(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var lines []string
|
||||
var err error
|
||||
if id == "" {
|
||||
// Launcher logs
|
||||
lines, err = a.mgr.UnifiedLogTail(100)
|
||||
} else {
|
||||
// Agent logs — in unified mode, all go to launcher log
|
||||
lines, err = a.mgr.UnifiedLogTail(100)
|
||||
}
|
||||
if err != nil {
|
||||
return puretui.MsgLogsLoaded{Lines: []string{"Error: " + err.Error()}}
|
||||
}
|
||||
return puretui.MsgLogsLoaded{Lines: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runGoTests() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
goBin = "/usr/local/go/bin/go"
|
||||
}
|
||||
cmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||
cmd.Env = a.mgr.BuildEnv()
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
output := strings.TrimSpace(string(out))
|
||||
if output == "" && err != nil {
|
||||
output = "Error: " + err.Error()
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindGo, Passed: err == nil, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runE2ETests(headed bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
args := []string{"./dev-scripts/e2e/run.sh"}
|
||||
if headed {
|
||||
args = append(args, "--headed")
|
||||
}
|
||||
cmd := exec.Command("bash", args...)
|
||||
cmd.Env = a.mgr.BuildEnv()
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
output := strings.TrimSpace(string(out))
|
||||
if output == "" && err != nil {
|
||||
output = "Error: " + err.Error()
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
kind := puretui.TestKindE2E
|
||||
if headed {
|
||||
kind = puretui.TestKindE2EHead
|
||||
}
|
||||
return puretui.MsgTestsDone{Kind: kind, Passed: err == nil, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runAllTests() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var allLines []string
|
||||
|
||||
// Go tests first
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
goBin = "/usr/local/go/bin/go"
|
||||
}
|
||||
goCmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||
goCmd.Env = a.mgr.BuildEnv()
|
||||
goOut, goErr := goCmd.CombinedOutput()
|
||||
|
||||
allLines = append(allLines, "═══ Go Tests ═══")
|
||||
goOutput := strings.TrimSpace(string(goOut))
|
||||
if goOutput == "" && goErr != nil {
|
||||
goOutput = "Error: " + goErr.Error()
|
||||
}
|
||||
allLines = append(allLines, strings.Split(goOutput, "\n")...)
|
||||
|
||||
if goErr != nil {
|
||||
allLines = append(allLines, "", "Go tests FAILED — skipping E2E")
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindAll, Passed: false, Output: allLines}
|
||||
}
|
||||
|
||||
// E2E tests
|
||||
allLines = append(allLines, "", "═══ E2E Tests ═══")
|
||||
e2eCmd := exec.Command("bash", "./dev-scripts/e2e/run.sh")
|
||||
e2eCmd.Env = a.mgr.BuildEnv()
|
||||
e2eOut, e2eErr := e2eCmd.CombinedOutput()
|
||||
|
||||
e2eOutput := strings.TrimSpace(string(e2eOut))
|
||||
if e2eOutput == "" && e2eErr != nil {
|
||||
e2eOutput = "Error: " + e2eErr.Error()
|
||||
}
|
||||
allLines = append(allLines, strings.Split(e2eOutput, "\n")...)
|
||||
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindAll, Passed: e2eErr == nil, Output: allLines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) tick() tea.Cmd {
|
||||
return tea.Tick(3*time.Second, func(time.Time) tea.Msg {
|
||||
return puretui.MsgTick{}
|
||||
})
|
||||
}
|
||||
|
||||
// ── formatting helpers ───────────────────────────────────────────────────
|
||||
|
||||
func formatUptime(secs int64) string {
|
||||
if secs < 0 {
|
||||
return "n/a"
|
||||
}
|
||||
d := secs / 86400
|
||||
h := (secs % 86400) / 3600
|
||||
m := (secs % 3600) / 60
|
||||
if d > 0 {
|
||||
return fmt.Sprintf("%dd %dh", d, h)
|
||||
}
|
||||
if h > 0 {
|
||||
return fmt.Sprintf("%dh %dm", h, m)
|
||||
}
|
||||
return fmt.Sprintf("%dm", m)
|
||||
}
|
||||
|
||||
func formatBytes(bytes int64) string {
|
||||
switch {
|
||||
case bytes >= 1<<30:
|
||||
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(1<<30))
|
||||
case bytes >= 1<<20:
|
||||
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(1<<20))
|
||||
case bytes >= 1<<10:
|
||||
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(1<<10))
|
||||
default:
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user