P2.2-P2.9, P3.2-P3.10, P4.1-P4.3: Complete Phases 2, 3, and 4

11 work units built in parallel and merged:

Agent handlers (Phase 2):
- P2.2 Deploy: pull images, stop/remove/run containers, update registry
- P2.3 Lifecycle: stop/start/restart with desired_state tracking
- P2.4 Status: list (registry), live check (runtime), get status (drift+events)
- P2.5 Sync: receive desired state, reconcile unmanaged containers
- P2.6 File transfer: push/pull scoped to /srv/<service>/, path validation
- P2.7 Adopt: match <service>-* containers, derive component names
- P2.8 Monitor: continuous watch loop, drift/flap alerting, event pruning
- P2.9 Snapshot: VACUUM INTO database backup command

CLI commands (Phase 3):
- P3.2 Login, P3.3 Deploy, P3.4 Stop/Start/Restart
- P3.5 List/Ps/Status, P3.6 Sync, P3.7 Adopt
- P3.8 Service show/edit/export, P3.9 Push/Pull, P3.10 Node list/add/remove

Deployment artifacts (Phase 4):
- Systemd units (agent service + backup timer)
- Example configs (CLI + agent)
- Install script (idempotent)

All packages: build, vet, lint (0 issues), test (all pass).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:21:18 -07:00
parent d7cc970133
commit 8f913ddf9b
33 changed files with 3593 additions and 62 deletions

View File

@@ -2,7 +2,12 @@ package main
import (
"fmt"
"os"
"text/tabwriter"
toml "github.com/pelletier/go-toml/v2"
"git.wntrmute.dev/kyle/mcp/internal/config"
"github.com/spf13/cobra"
)
@@ -15,29 +20,112 @@ func nodeCmd() *cobra.Command {
list := &cobra.Command{
Use: "list",
Short: "List registered nodes",
RunE: func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("not implemented")
},
RunE: runNodeList,
}
add := &cobra.Command{
Use: "add <name> <address>",
Short: "Register a node",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("not implemented")
},
RunE: runNodeAdd,
}
remove := &cobra.Command{
Use: "remove <name>",
Short: "Deregister a node",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("not implemented")
},
RunE: runNodeRemove,
}
cmd.AddCommand(list, add, remove)
return cmd
}
func runNodeList(_ *cobra.Command, _ []string) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "NAME\tADDRESS")
for _, n := range cfg.Nodes {
_, _ = fmt.Fprintf(w, "%s\t%s\n", n.Name, n.Address)
}
return w.Flush()
}
func runNodeAdd(_ *cobra.Command, args []string) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
name := args[0]
address := args[1]
for _, n := range cfg.Nodes {
if n.Name == name {
return fmt.Errorf("node %q already exists", name)
}
}
cfg.Nodes = append(cfg.Nodes, config.NodeConfig{
Name: name,
Address: address,
})
if err := writeConfig(cfgPath, cfg); err != nil {
return fmt.Errorf("write config: %w", err)
}
fmt.Printf("Added node %s (%s)\n", name, address)
return nil
}
func runNodeRemove(_ *cobra.Command, args []string) error {
cfg, err := config.LoadCLIConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
name := args[0]
var found bool
nodes := make([]config.NodeConfig, 0, len(cfg.Nodes))
for _, n := range cfg.Nodes {
if n.Name == name {
found = true
continue
}
nodes = append(nodes, n)
}
if !found {
return fmt.Errorf("node %q not found", name)
}
cfg.Nodes = nodes
if err := writeConfig(cfgPath, cfg); err != nil {
return fmt.Errorf("write config: %w", err)
}
fmt.Printf("Removed node %s\n", name)
return nil
}
// writeConfig serializes the CLIConfig to TOML and writes it back to the
// config file.
func writeConfig(path string, cfg *config.CLIConfig) error {
data, err := toml.Marshal(cfg)
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
return fmt.Errorf("write config %q: %w", path, err)
}
return nil
}