400 lines
9.4 KiB
Go
400 lines
9.4 KiB
Go
// 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"
|
|
"strings"
|
|
"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.IntentStartAgent:
|
|
return a.startAgent(intent.AgentID)
|
|
|
|
case puretui.IntentStopAgent:
|
|
return a.stopAgent(intent.AgentID)
|
|
|
|
case puretui.IntentKillAgent:
|
|
return a.killAgent(intent.AgentID)
|
|
|
|
case puretui.IntentRestartAgent:
|
|
return a.restartAgent(intent.AgentID)
|
|
|
|
case puretui.IntentLoadLogs:
|
|
return a.loadLogs(intent.AgentID)
|
|
|
|
case puretui.IntentStartAll:
|
|
return a.startAll()
|
|
|
|
case puretui.IntentStopAll:
|
|
return a.stopAll()
|
|
|
|
case puretui.IntentRestartAll:
|
|
return a.restartAll()
|
|
|
|
case puretui.IntentKillAll:
|
|
return a.killAll()
|
|
|
|
case puretui.IntentRebuildRestart:
|
|
return a.rebuildRestart()
|
|
|
|
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.StatusAll()
|
|
if err != nil {
|
|
return puretui.MsgAgentsLoaded{}
|
|
}
|
|
|
|
views := make([]puretui.AgentView, len(statuses))
|
|
for i, s := range statuses {
|
|
v := puretui.AgentView{
|
|
ID: s.ID,
|
|
Name: s.Name,
|
|
Version: s.Version,
|
|
Desc: s.Desc,
|
|
Enabled: s.Enabled,
|
|
Running: s.Running,
|
|
PID: s.PID,
|
|
Instances: s.Instances,
|
|
}
|
|
|
|
if s.Running {
|
|
if stats, err := a.mgr.Stats(s.ID); err == nil {
|
|
v.Uptime = formatUptime(stats.UptimeSecs)
|
|
v.Memory = formatBytes(stats.MemRSSKB * 1024)
|
|
v.CPU = fmt.Sprintf("%.1f%%", stats.CPUPct)
|
|
v.LogSize = formatBytes(stats.LogBytes)
|
|
}
|
|
}
|
|
|
|
views[i] = v
|
|
}
|
|
|
|
return puretui.MsgAgentsLoaded{Agents: views}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) startAgent(id string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
agents, err := a.mgr.Scan()
|
|
if err != nil {
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Start", Err: err}
|
|
}
|
|
for _, agent := range agents {
|
|
if agent.ID == id {
|
|
err = a.mgr.Start(agent)
|
|
// Give the process a moment to start.
|
|
if err == nil {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Start", Err: err}
|
|
}
|
|
}
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Start", Err: fmt.Errorf("agent not found")}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) stopAgent(id string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
err := a.mgr.Stop(id)
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Stop", Err: err}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) killAgent(id string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
err := a.mgr.Kill(id)
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Kill", Err: err}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) restartAgent(id string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
// Stop first (ignore error if not running)
|
|
_ = a.mgr.Stop(id)
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
agents, err := a.mgr.Scan()
|
|
if err != nil {
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err}
|
|
}
|
|
for _, agent := range agents {
|
|
if agent.ID == id {
|
|
err = a.mgr.Start(agent)
|
|
if err == nil {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err}
|
|
}
|
|
}
|
|
return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: fmt.Errorf("agent not found")}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) startAll() tea.Cmd {
|
|
return func() tea.Msg {
|
|
agents, err := a.mgr.Scan()
|
|
if err != nil {
|
|
return puretui.MsgServerActionDone{Action: "Start All", Errors: []string{err.Error()}, Failed: 1}
|
|
}
|
|
var total, failed int
|
|
var errs []string
|
|
for _, agent := range agents {
|
|
if !agent.Enabled {
|
|
continue
|
|
}
|
|
if a.mgr.IsRunning(agent.ID) {
|
|
continue
|
|
}
|
|
total++
|
|
if err := a.mgr.Start(agent); err != nil {
|
|
failed++
|
|
errs = append(errs, fmt.Sprintf("%s: %v", agent.ID, err))
|
|
}
|
|
}
|
|
if total > 0 {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return puretui.MsgServerActionDone{Action: "Start All", Total: total, Failed: failed, Errors: errs}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) stopAll() tea.Cmd {
|
|
return func() tea.Msg {
|
|
statuses, err := a.mgr.StatusAll()
|
|
if err != nil {
|
|
return puretui.MsgServerActionDone{Action: "Stop All", Errors: []string{err.Error()}, Failed: 1}
|
|
}
|
|
var total, failed int
|
|
var errs []string
|
|
for _, s := range statuses {
|
|
if !s.Running {
|
|
continue
|
|
}
|
|
total++
|
|
if err := a.mgr.Stop(s.ID); err != nil {
|
|
failed++
|
|
errs = append(errs, fmt.Sprintf("%s: %v", s.ID, err))
|
|
}
|
|
}
|
|
return puretui.MsgServerActionDone{Action: "Stop All", Total: total, Failed: failed, Errors: errs}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) restartAll() tea.Cmd {
|
|
return func() tea.Msg {
|
|
agents, err := a.mgr.Scan()
|
|
if err != nil {
|
|
return puretui.MsgServerActionDone{Action: "Restart All", Errors: []string{err.Error()}, Failed: 1}
|
|
}
|
|
|
|
// Stop all running first
|
|
for _, agent := range agents {
|
|
if agent.Enabled && a.mgr.IsRunning(agent.ID) {
|
|
_ = a.mgr.Stop(agent.ID)
|
|
}
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
// Start all enabled
|
|
var total, failed int
|
|
var errs []string
|
|
for _, agent := range agents {
|
|
if !agent.Enabled {
|
|
continue
|
|
}
|
|
total++
|
|
if err := a.mgr.Start(agent); err != nil {
|
|
failed++
|
|
errs = append(errs, fmt.Sprintf("%s: %v", agent.ID, err))
|
|
}
|
|
}
|
|
if total > 0 {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return puretui.MsgServerActionDone{Action: "Restart All", Total: total, Failed: failed, Errors: errs}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) killAll() tea.Cmd {
|
|
return func() tea.Msg {
|
|
statuses, err := a.mgr.StatusAll()
|
|
if err != nil {
|
|
return puretui.MsgServerActionDone{Action: "Kill All", Errors: []string{err.Error()}, Failed: 1}
|
|
}
|
|
var total, failed int
|
|
var errs []string
|
|
for _, s := range statuses {
|
|
if !s.Running {
|
|
continue
|
|
}
|
|
total++
|
|
if err := a.mgr.Kill(s.ID); err != nil {
|
|
failed++
|
|
errs = append(errs, fmt.Sprintf("%s: %v", s.ID, err))
|
|
}
|
|
}
|
|
return puretui.MsgServerActionDone{Action: "Kill All", Total: total, Failed: failed, Errors: errs}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) rebuildRestart() tea.Cmd {
|
|
return func() tea.Msg {
|
|
// 1. Remember which agents are currently running
|
|
statuses, err := a.mgr.StatusAll()
|
|
if err != nil {
|
|
return puretui.MsgRebuildDone{Errors: []string{err.Error()}}
|
|
}
|
|
var wasRunning []string
|
|
for _, s := range statuses {
|
|
if s.Running {
|
|
wasRunning = append(wasRunning, s.ID)
|
|
}
|
|
}
|
|
|
|
// 2. Stop all running agents
|
|
for _, id := range wasRunning {
|
|
_ = a.mgr.Stop(id)
|
|
}
|
|
if len(wasRunning) > 0 {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
// 3. Build
|
|
buildOut, buildErr := a.mgr.Build()
|
|
if buildErr != nil {
|
|
// Build failed — try to restart what was running before
|
|
for _, id := range wasRunning {
|
|
agents, _ := a.mgr.Scan()
|
|
for _, ag := range agents {
|
|
if ag.ID == id {
|
|
_ = a.mgr.Start(ag)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Return last lines of build output
|
|
lines := strings.Split(strings.TrimSpace(buildOut), "\n")
|
|
tail := buildOut
|
|
if len(lines) > 5 {
|
|
tail = strings.Join(lines[len(lines)-5:], "\n")
|
|
}
|
|
return puretui.MsgRebuildDone{BuildLog: tail}
|
|
}
|
|
|
|
// 4. Restart only the agents that were running before
|
|
agents, _ := a.mgr.Scan()
|
|
agentMap := make(map[string]process.AgentInfo)
|
|
for _, ag := range agents {
|
|
agentMap[ag.ID] = ag
|
|
}
|
|
|
|
var restarted, failed int
|
|
var errs []string
|
|
for _, id := range wasRunning {
|
|
ag, ok := agentMap[id]
|
|
if !ok {
|
|
failed++
|
|
errs = append(errs, fmt.Sprintf("%s: not found after build", id))
|
|
continue
|
|
}
|
|
if err := a.mgr.Start(ag); err != nil {
|
|
failed++
|
|
errs = append(errs, fmt.Sprintf("%s: %v", id, err))
|
|
} else {
|
|
restarted++
|
|
}
|
|
}
|
|
if restarted > 0 {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
return puretui.MsgRebuildDone{
|
|
BuildOK: true,
|
|
Restarted: restarted,
|
|
Failed: failed,
|
|
Errors: errs,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) loadLogs(id string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
lines, err := a.mgr.LogTail(id, 100)
|
|
if err != nil {
|
|
return puretui.MsgLogsLoaded{Lines: []string{"Error: " + err.Error()}}
|
|
}
|
|
return puretui.MsgLogsLoaded{Lines: lines}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|