// Package projects provides a registry of claudebox projects. package projects import ( "context" "fmt" "os/exec" "strings" "sync" ) // Project represents a claudebox project. type Project struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` PodName string `json:"pod"` Status string `json:"status"` Workspace string `json:"workspace,omitempty"` } // Registry manages the list of available projects. type Registry struct { namespace string projects map[string]*Project mu sync.RWMutex } // NewRegistry creates a new project registry. func NewRegistry(namespace string) *Registry { r := &Registry{ namespace: namespace, projects: make(map[string]*Project), } // Initialize with known projects // In the future, this could discover projects from K8s labels r.projects["pantheon"] = &Project{ ID: "pantheon", Name: "Pantheon", Description: "Go API backend", PodName: "claudebox-pantheon-0", Status: "unknown", Workspace: "/workspace", } r.projects["aeries"] = &Project{ ID: "aeries", Name: "Aeries", Description: "Note community platform", PodName: "claudebox-aeries-0", Status: "unknown", Workspace: "/workspace", } return r } // List returns all projects. func (r *Registry) List() []*Project { r.mu.RLock() defer r.mu.RUnlock() projects := make([]*Project, 0, len(r.projects)) for _, p := range r.projects { projects = append(projects, p) } return projects } // Get returns a project by ID. func (r *Registry) Get(id string) (*Project, bool) { r.mu.RLock() defer r.mu.RUnlock() p, ok := r.projects[id] return p, ok } // Exists checks if a project exists. func (r *Registry) Exists(id string) bool { r.mu.RLock() defer r.mu.RUnlock() _, ok := r.projects[id] return ok } // RefreshStatus updates the status of all projects from K8s. func (r *Registry) RefreshStatus(ctx context.Context) error { r.mu.Lock() defer r.mu.Unlock() for _, p := range r.projects { status, err := getPodStatus(ctx, r.namespace, p.PodName) if err != nil { p.Status = "error" continue } p.Status = status } return nil } // getPodStatus queries the status of a pod. func getPodStatus(ctx context.Context, namespace, podName string) (string, error) { cmd := exec.CommandContext(ctx, "kubectl", "get", "pod", podName, "-n", namespace, "-o", "jsonpath={.status.phase}", ) output, err := cmd.Output() if err != nil { // Check if pod doesn't exist if strings.Contains(err.Error(), "not found") { return "not_found", nil } return "unknown", fmt.Errorf("get pod status: %w", err) } phase := strings.ToLower(strings.TrimSpace(string(output))) switch phase { case "running": return "running", nil case "pending": return "pending", nil case "succeeded": return "completed", nil case "failed": return "failed", nil default: return phase, nil } } // Register adds a new project to the registry. func (r *Registry) Register(p *Project) { r.mu.Lock() defer r.mu.Unlock() r.projects[p.ID] = p } // Unregister removes a project from the registry. func (r *Registry) Unregister(id string) { r.mu.Lock() defer r.mu.Unlock() delete(r.projects, id) }