package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "os" "path/filepath" "text/tabwriter" ) func main() { home, err := os.UserHomeDir() if err != nil { log.Fatalf("Could not locate user home dir: %v", err) } configPath := filepath.Join(home, ".config", "vssm", "config.json") cfg, err := LoadOrCreateConfig(configPath) if err != nil { log.Fatalf("Initialization failed: %v", err) } if len(os.Args) < 2 { printUsage() return } subCommand := os.Args[1] switch subCommand { case "daemon": fmt.Println("Initializing VS server manager background supervisor...") if err := StartDaemon(cfg); err != nil { log.Fatalf("Daemon runtime fatal error: %v", err) } case "create": if len(os.Args) < 4 { log.Fatalf("Usage: go run . create ") } name := os.Args[2] version := os.Args[3] sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/create?name=%s&version=%s", url.QueryEscape(name), url.QueryEscape(version)), nil) case "start": if len(os.Args) < 3 { log.Fatalf("Usage: go run . start ") } name := os.Args[2] sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/start?name=%s", url.QueryEscape(name)), nil) case "stop": if len(os.Args) < 3 { log.Fatalf("Usage: go run . stop ") } name := os.Args[2] sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/stop?name=%s", url.QueryEscape(name)), nil) case "cmd": if len(os.Args) < 4 { log.Fatalf("Usage: go run . cmd \"\"") } name := os.Args[2] serverCmd := os.Args[3] payload := CommandPayload{Command: serverCmd} body, _ := json.Marshal(payload) sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/command?name=%s", url.QueryEscape(name)), bytes.NewBuffer(body)) case "list", "status": fetchAndPrintStatus(cfg) default: printUsage() } } func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) { targetUrl := fmt.Sprintf("http://%s%s", cfg.Daemon.ListenAddress, path) req, err := http.NewRequest(method, targetUrl, body) if err != nil { log.Fatalf("Failed to construct IPC frame: %v", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatalf("IPC connection failed. Is the vs-manager daemon running? Error: %v", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { log.Fatalf("Error from daemon: %s", string(respBody)) } fmt.Println(string(respBody)) } func fetchAndPrintStatus(cfg *AppConfig) { targetUrl := fmt.Sprintf("http://%s/instances/list", cfg.Daemon.ListenAddress) resp, err := http.Get(targetUrl) if err != nil { log.Fatalf("IPC connection failed. Is the vs-manager daemon running? Error: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) log.Fatalf("Error from daemon: %s", string(respBody)) } var instances []InstanceStatusResponse if err := json.NewDecoder(resp.Body).Decode(&instances); err != nil { log.Fatalf("Failed to decode daemon status matrix: %v", err) } if len(instances) == 0 { fmt.Println("No instances configured yet. Use 'create' to provision one.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) fmt.Fprintln(w, "INSTANCE NAME\tVERSION\tPORT\tSTATUS") fmt.Fprintln(w, "-------------\t-------\t----\t------") for _, inst := range instances { fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", inst.Name, inst.Version, inst.Port, inst.Status) } w.Flush() } func printUsage() { fmt.Println("VintageStory Server Manager") fmt.Println("\nUsage:") fmt.Println(" vssm daemon Starts the background process supervisor") fmt.Println(" vssm create Provisions baseline configuration and stores instance profile") fmt.Println(" vssm start Launches an existing server instance using stored profile") fmt.Println(" vssm stop Gracefully shuts down a server instance") fmt.Println(" vssm cmd \"\" Dispatches a terminal console command down the pipe") }