|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "flag" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | + "strings" |
| 9 | +) |
| 10 | + |
| 11 | +// serviceManager abstracts OS-level service operations. |
| 12 | +type serviceManager interface { |
| 13 | + Install(cfg serviceConfig) error |
| 14 | + Uninstall() error |
| 15 | + Status() (string, error) |
| 16 | + Logs(lines int, follow bool) error |
| 17 | +} |
| 18 | + |
| 19 | +// serviceConfig holds configuration for the managed service. |
| 20 | +type serviceConfig struct { |
| 21 | + BinaryPath string // absolute path to the peerclaw binary |
| 22 | + ServerURL string // PeerClaw server URL |
| 23 | + AgentID string // agent identity (required for heartbeat mode) |
| 24 | + Mode string // "mcp" or "heartbeat" |
| 25 | + Force bool // overwrite existing service |
| 26 | +} |
| 27 | + |
| 28 | +// execArgs returns the command-line arguments for the service process. |
| 29 | +func (c serviceConfig) execArgs() []string { |
| 30 | + switch c.Mode { |
| 31 | + case "heartbeat": |
| 32 | + return []string{c.BinaryPath, "agent", "heartbeat", c.AgentID, "--loop", "--server", c.ServerURL} |
| 33 | + default: // "mcp" |
| 34 | + return []string{c.BinaryPath, "mcp", "serve", "--server", c.ServerURL} |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +// RunService handles the "service" subcommand. |
| 39 | +func RunService(args []string, serverURL string) int { |
| 40 | + if len(args) < 1 { |
| 41 | + printServiceUsage() |
| 42 | + return 1 |
| 43 | + } |
| 44 | + |
| 45 | + mgr := newServiceManager() |
| 46 | + |
| 47 | + switch args[0] { |
| 48 | + case "install": |
| 49 | + return runServiceInstall(args[1:], serverURL, mgr) |
| 50 | + case "uninstall": |
| 51 | + return runServiceUninstall(mgr) |
| 52 | + case "status": |
| 53 | + return runServiceStatus(mgr) |
| 54 | + case "logs": |
| 55 | + return runServiceLogs(args[1:], mgr) |
| 56 | + default: |
| 57 | + fmt.Fprintf(os.Stderr, "unknown service command: %s\n\n", args[0]) |
| 58 | + printServiceUsage() |
| 59 | + return 1 |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +func runServiceInstall(args []string, serverURL string, mgr serviceManager) int { |
| 64 | + fs := flag.NewFlagSet("service install", flag.ContinueOnError) |
| 65 | + mode := fs.String("mode", "mcp", "Service mode: mcp (default) or heartbeat") |
| 66 | + force := fs.Bool("force", false, "Overwrite existing service file") |
| 67 | + addServerFlag(fs, &serverURL) |
| 68 | + |
| 69 | + if err := fs.Parse(args); err != nil { |
| 70 | + return 1 |
| 71 | + } |
| 72 | + |
| 73 | + if *mode != "mcp" && *mode != "heartbeat" { |
| 74 | + fmt.Fprintf(os.Stderr, "Error: invalid mode %q (must be mcp or heartbeat)\n", *mode) |
| 75 | + return 1 |
| 76 | + } |
| 77 | + |
| 78 | + // Resolve binary path. |
| 79 | + exe, err := os.Executable() |
| 80 | + if err != nil { |
| 81 | + fmt.Fprintf(os.Stderr, "Error: cannot determine binary path: %v\n", err) |
| 82 | + return 1 |
| 83 | + } |
| 84 | + binPath, err := filepath.EvalSymlinks(exe) |
| 85 | + if err != nil { |
| 86 | + fmt.Fprintf(os.Stderr, "Error: cannot resolve binary path: %v\n", err) |
| 87 | + return 1 |
| 88 | + } |
| 89 | + |
| 90 | + // Warn if binary is in a temporary location. |
| 91 | + if strings.Contains(binPath, "/tmp/") || strings.Contains(binPath, "go-build") { |
| 92 | + fmt.Fprintf(os.Stderr, "Warning: binary path %q looks temporary — the service may break after cleanup\n", binPath) |
| 93 | + } |
| 94 | + |
| 95 | + // Load config for agent_id. |
| 96 | + cfg, _ := loadCLIConfig() |
| 97 | + agentID := "" |
| 98 | + if cfg != nil { |
| 99 | + agentID = cfg.AgentID |
| 100 | + } |
| 101 | + |
| 102 | + if *mode == "heartbeat" && agentID == "" { |
| 103 | + fmt.Fprintln(os.Stderr, "Error: heartbeat mode requires agent_id in config") |
| 104 | + fmt.Fprintln(os.Stderr, "Run: peerclaw agent claim <name> --server <url>") |
| 105 | + return 1 |
| 106 | + } |
| 107 | + |
| 108 | + svcCfg := serviceConfig{ |
| 109 | + BinaryPath: binPath, |
| 110 | + ServerURL: serverURL, |
| 111 | + AgentID: agentID, |
| 112 | + Mode: *mode, |
| 113 | + Force: *force, |
| 114 | + } |
| 115 | + |
| 116 | + if err := mgr.Install(svcCfg); err != nil { |
| 117 | + fmt.Fprintf(os.Stderr, "Error: %v\n", err) |
| 118 | + return 1 |
| 119 | + } |
| 120 | + |
| 121 | + fmt.Println("Service installed and started successfully.") |
| 122 | + fmt.Printf(" Mode: %s\n", svcCfg.Mode) |
| 123 | + fmt.Printf(" Binary: %s\n", svcCfg.BinaryPath) |
| 124 | + fmt.Printf(" Server: %s\n", svcCfg.ServerURL) |
| 125 | + if svcCfg.AgentID != "" { |
| 126 | + fmt.Printf(" Agent: %s\n", svcCfg.AgentID) |
| 127 | + } |
| 128 | + fmt.Println() |
| 129 | + fmt.Println("Manage with:") |
| 130 | + fmt.Println(" peerclaw service status") |
| 131 | + fmt.Println(" peerclaw service logs -f") |
| 132 | + fmt.Println(" peerclaw service uninstall") |
| 133 | + return 0 |
| 134 | +} |
| 135 | + |
| 136 | +func runServiceUninstall(mgr serviceManager) int { |
| 137 | + if err := mgr.Uninstall(); err != nil { |
| 138 | + fmt.Fprintf(os.Stderr, "Error: %v\n", err) |
| 139 | + return 1 |
| 140 | + } |
| 141 | + fmt.Println("Service uninstalled.") |
| 142 | + return 0 |
| 143 | +} |
| 144 | + |
| 145 | +func runServiceStatus(mgr serviceManager) int { |
| 146 | + status, err := mgr.Status() |
| 147 | + if err != nil { |
| 148 | + fmt.Fprintf(os.Stderr, "Error: %v\n", err) |
| 149 | + return 1 |
| 150 | + } |
| 151 | + fmt.Print(status) |
| 152 | + return 0 |
| 153 | +} |
| 154 | + |
| 155 | +func runServiceLogs(args []string, mgr serviceManager) int { |
| 156 | + fs := flag.NewFlagSet("service logs", flag.ContinueOnError) |
| 157 | + lines := fs.Int("n", 50, "Number of log lines to show") |
| 158 | + follow := fs.Bool("f", false, "Follow log output") |
| 159 | + |
| 160 | + if err := fs.Parse(args); err != nil { |
| 161 | + return 1 |
| 162 | + } |
| 163 | + |
| 164 | + if err := mgr.Logs(*lines, *follow); err != nil { |
| 165 | + fmt.Fprintf(os.Stderr, "Error: %v\n", err) |
| 166 | + return 1 |
| 167 | + } |
| 168 | + return 0 |
| 169 | +} |
| 170 | + |
| 171 | +func printServiceUsage() { |
| 172 | + fmt.Fprintf(os.Stderr, `Usage: peerclaw service <command> [options] |
| 173 | +
|
| 174 | +Commands: |
| 175 | + install Install and start the agent as an OS service |
| 176 | + uninstall Stop and remove the agent service |
| 177 | + status Show service status |
| 178 | + logs View service logs |
| 179 | +
|
| 180 | +Options (install): |
| 181 | + --mode Service mode: mcp (default) or heartbeat |
| 182 | + --force Overwrite existing service file |
| 183 | + --server PeerClaw server URL |
| 184 | +
|
| 185 | +Options (logs): |
| 186 | + -n Number of lines to show (default: 50) |
| 187 | + -f Follow log output |
| 188 | +
|
| 189 | +Examples: |
| 190 | + peerclaw service install |
| 191 | + peerclaw service install --mode heartbeat |
| 192 | + peerclaw service install --force |
| 193 | + peerclaw service status |
| 194 | + peerclaw service logs -n 20 -f |
| 195 | + peerclaw service uninstall |
| 196 | +`) |
| 197 | +} |
0 commit comments