package main import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" ) type InstanceInfo struct { ID string `json:"id"` TemplateName string `json:"templateName,omitempty"` Port int `json:"port"` Status string `json:"status"` } func instancesFile(dataDir string) string { return filepath.Join(dataDir, "instances.json") } func loadInstances(dataDir string) (map[string]*InstanceInfo, error) { f := instancesFile(dataDir) data, err := os.ReadFile(f) if err != nil { if os.IsNotExist(err) { return make(map[string]*InstanceInfo), nil } return nil, err } var list []*InstanceInfo if err := json.Unmarshal(data, &list); err != nil { return nil, err } m := make(map[string]*InstanceInfo) for _, inst := range list { m[inst.ID] = inst } return m, nil } func saveInstances(dataDir string, instances map[string]*InstanceInfo) error { if err := os.MkdirAll(dataDir, 0755); err != nil { return err } list := make([]*InstanceInfo, 0, len(instances)) for _, inst := range instances { list = append(list, inst) } data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } return os.WriteFile(instancesFile(dataDir), data, 0644) } func upsertInstance(dataDir string, inst *InstanceInfo) error { instances, err := loadInstances(dataDir) if err != nil { return err } instances[inst.ID] = inst return saveInstances(dataDir, instances) } func removeInstance(dataDir, instanceID string) error { instances, err := loadInstances(dataDir) if err != nil { return err } delete(instances, instanceID) return saveInstances(dataDir, instances) } func instanceURL(inst *InstanceInfo) string { return fmt.Sprintf("http://localhost:%d", inst.Port) } func getInstanceStatus(dataDir, instanceID string) string { dir := instanceDir(dataDir, instanceID) composeFile := filepath.Join(dir, "docker-compose.yml") if _, err := os.Stat(composeFile); err != nil { return "stopped" } engine := getContainerEngine() // Try modern JSON format first cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json") out, err := cmd.Output() if err == nil { outStr := strings.TrimSpace(string(out)) if outStr == "" || outStr == "[]" { return "stopped" } if strings.HasPrefix(outStr, "[") { var containers []map[string]interface{} if err := json.Unmarshal(out, &containers); err == nil { for _, c := range containers { state, _ := c["State"].(string) if state == "running" { return "running" } } return "stopped" } } else { var c map[string]interface{} if err := json.Unmarshal(out, &c); err == nil { state, _ := c["State"].(string) if state == "running" { return "running" } return "stopped" } } } // Fallback: use "ps -q" which is supported by all docker-compose versions cmd = exec.Command(engine, "compose", "-f", composeFile, "ps", "-q") out, err = cmd.Output() if err != nil { return "error" } if strings.TrimSpace(string(out)) != "" { return "running" } return "stopped" }