Files
mcp/cmd/mcp/status.go
Kyle Isom 08b3e2a472 Migrate module path from kyle/ to mc/ org
All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0,
mc-proxy to v1.1.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:07:42 -07:00

238 lines
6.2 KiB
Go

package main
import (
"context"
"fmt"
"os"
"text/tabwriter"
"time"
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
"git.wntrmute.dev/mc/mcp/internal/config"
"github.com/spf13/cobra"
)
// newTable returns a tabwriter configured for CLI table output.
func newTable() *tabwriter.Writer {
return tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
}
// forEachNode loads the CLI config, then calls fn for every configured node.
// Dial errors are printed as warnings and the node is skipped. Connections
// are closed before moving to the next node, avoiding defer-in-loop leaks.
func forEachNode(fn func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
for _, node := range cfg.Nodes {
client, conn, err := dialAgent(node.Address, cfg)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: %v\n", node.Name, err)
continue
}
fnErr := fn(node, client)
_ = conn.Close()
if fnErr != nil {
return fnErr
}
}
return nil
}
func listCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List services from all agents (registry, no runtime query)",
RunE: func(cmd *cobra.Command, args []string) error {
w := newTable()
_, _ = fmt.Fprintln(w, "SERVICE\tCOMPONENT\tDESIRED\tOBSERVED\tVERSION")
if err := forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
resp, err := client.ListServices(context.Background(), &mcpv1.ListServicesRequest{})
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: list services: %v\n", node.Name, err)
return nil
}
for _, svc := range resp.GetServices() {
for _, comp := range svc.GetComponents() {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
svc.GetName(),
comp.GetName(),
comp.GetDesiredState(),
comp.GetObservedState(),
comp.GetVersion(),
)
}
}
return nil
}); err != nil {
return err
}
return w.Flush()
},
}
}
func psCmd() *cobra.Command {
return &cobra.Command{
Use: "ps",
Short: "Live check: query runtime on all agents",
RunE: func(cmd *cobra.Command, args []string) error {
w := newTable()
_, _ = fmt.Fprintln(w, "SERVICE\tCOMPONENT\tNODE\tSTATE\tVERSION\tUPTIME")
now := time.Now()
if err := forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
resp, err := client.LiveCheck(context.Background(), &mcpv1.LiveCheckRequest{})
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: live check: %v\n", node.Name, err)
return nil
}
for _, svc := range resp.GetServices() {
for _, comp := range svc.GetComponents() {
uptime := "-"
if comp.GetStarted() != nil {
d := now.Sub(comp.GetStarted().AsTime())
uptime = formatDuration(d)
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
svc.GetName(),
comp.GetName(),
node.Name,
comp.GetObservedState(),
comp.GetVersion(),
uptime,
)
}
}
return nil
}); err != nil {
return err
}
return w.Flush()
},
}
}
func statusCmd() *cobra.Command {
return &cobra.Command{
Use: "status [service]",
Short: "Full picture: live query + drift + recent events",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var serviceName string
if len(args) > 0 {
serviceName = args[0]
}
var allServices []*mcpv1.ServiceInfo
var allDrift []*mcpv1.DriftInfo
var allEvents []*mcpv1.EventInfo
if err := forEachNode(func(node config.NodeConfig, client mcpv1.McpAgentServiceClient) error {
resp, err := client.GetServiceStatus(context.Background(), &mcpv1.GetServiceStatusRequest{
Name: serviceName,
})
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "warning: %s: get service status: %v\n", node.Name, err)
return nil
}
allServices = append(allServices, resp.GetServices()...)
allDrift = append(allDrift, resp.GetDrift()...)
allEvents = append(allEvents, resp.GetRecentEvents()...)
return nil
}); err != nil {
return err
}
// Services table.
w := newTable()
_, _ = fmt.Fprintln(w, "SERVICE\tCOMPONENT\tDESIRED\tOBSERVED\tVERSION")
for _, svc := range allServices {
for _, comp := range svc.GetComponents() {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
svc.GetName(),
comp.GetName(),
comp.GetDesiredState(),
comp.GetObservedState(),
comp.GetVersion(),
)
}
}
if err := w.Flush(); err != nil {
return fmt.Errorf("flush services table: %w", err)
}
// Drift section.
if len(allDrift) > 0 {
fmt.Println()
fmt.Println("DRIFT:")
dw := newTable()
_, _ = fmt.Fprintln(dw, "SERVICE\tCOMPONENT\tDESIRED\tOBSERVED")
for _, d := range allDrift {
_, _ = fmt.Fprintf(dw, "%s\t%s\t%s\t%s\n",
d.GetService(),
d.GetComponent(),
d.GetDesiredState(),
d.GetObservedState(),
)
}
if err := dw.Flush(); err != nil {
return fmt.Errorf("flush drift table: %w", err)
}
}
// Recent events section.
if len(allEvents) > 0 {
fmt.Println()
fmt.Println("RECENT EVENTS:")
ew := newTable()
_, _ = fmt.Fprintln(ew, "SERVICE\tCOMPONENT\tPREV\tNEW\tTIME")
for _, e := range allEvents {
ts := "-"
if e.GetTimestamp() != nil {
ts = e.GetTimestamp().AsTime().Format(time.RFC3339)
}
_, _ = fmt.Fprintf(ew, "%s\t%s\t%s\t%s\t%s\n",
e.GetService(),
e.GetComponent(),
e.GetPrevState(),
e.GetNewState(),
ts,
)
}
if err := ew.Flush(); err != nil {
return fmt.Errorf("flush events table: %w", err)
}
}
return nil
},
}
}
// formatDuration returns a human-readable duration string.
func formatDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60)
}
if d < 24*time.Hour {
return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60)
}
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
return fmt.Sprintf("%dd%dh", days, hours)
}