package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "log" "net/http" "net/url" "os" "path/filepath" "strconv" "text/tabwriter" ) func main() { home, err := os.UserHomeDir() if err != nil { log.Fatalf("Could not locate user home dir: %v", err) } defaultConfigPath := filepath.Join(home, ".config", "vssm", "config.json") configFlag := flag.String("config", defaultConfigPath, "Explicit path targeting a custom vssm config.json profile") flag.Usage = printUsage flag.Parse() args := flag.Args() if len(args) < 1 { printUsage() return } configExplicitlySet := false flag.Visit(func(f *flag.Flag) { if f.Name == "config" { configExplicitlySet = true } }) if !configExplicitlySet { if envConfig := os.Getenv("VSSM_CONFIG_PATH"); envConfig != "" { configFlag = &envConfig } } absConfigPath, err := filepath.Abs(*configFlag) if err != nil { log.Fatalf("Failed to resolve absolute configuration path target: %v", err) } if configExplicitlySet { if err := os.Setenv("VSSM_CONFIG_PATH", absConfigPath); err != nil { log.Fatalf("Warning: could not set VSSM_CONFIG_PATH: %v", err) } } cfg, err := LoadOrCreateConfig(absConfigPath) if err != nil { log.Fatalf("Initialization failed: %v", err) } subCommand := args[0] switch subCommand { case "daemon": fmt.Printf("Initializing VS server manager background supervisor [Config: %s]...\n", absConfigPath) if err := StartDaemon(cfg, absConfigPath); err != nil { log.Fatalf("Daemon runtime fatal error: %v", err) } case "create": if len(args) < 4 { log.Fatalf("Usage: vssm create ") } name := args[1] version := args[2] port := args[3] sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/create?name=%s&version=%s&port=%s", url.QueryEscape(name), url.QueryEscape(version), url.QueryEscape(port)), nil) case "start": if len(args) < 2 { log.Fatalf("Usage: vssm start ") } name := args[1] sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/start?name=%s", url.QueryEscape(name)), nil) case "stop": if len(args) < 2 { log.Fatalf("Usage: vssm stop ") } name := args[1] sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/stop?name=%s", url.QueryEscape(name)), nil) case "cmd": if len(args) < 3 { log.Fatalf("Usage: vssm cmd \"\"") } name := args[1] serverCmd := args[2] 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) case "show-config": fmt.Printf("%v", cfg.Storage.AppDataDir) case "delete": if len(args) < 2 { log.Fatalf("Usage: vssm delete ") } name := args[1] deleteCmd := flag.NewFlagSet("delete", flag.ExitOnError) purge := deleteCmd.Bool("purge", false, "Delete instance files from disk") deleteCmd.Parse(args[2:]) sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/delete?name=%s&purge=%s", url.QueryEscape(name), url.QueryEscape(strconv.FormatBool(*purge))), nil) 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 && resp.StatusCode != http.StatusCreated { 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("\nGlobal Options:") fmt.Println(" --config Explicitly target a non-default config structure file location") fmt.Println("\nUsage Commands:") 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") fmt.Println(" vssm list Displays the operational matrix of all instances") }