package workspace import ( "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "github.com/samber/mo" ) type Workspace struct { HostPath string `json:"host_path"` Hash string `json:"hash"` MountTag string `json:"mount_tag"` GuestPath string `json:"guest_path"` } type Registry struct { filePath string workspaces map[string]Workspace } // Hash generates an 8-character hash from a path, matching bash behavior: // echo -n "$path" | sha256sum | cut -c1-8 func Hash(path string) string { h := sha256.Sum256([]byte(path)) return fmt.Sprintf("%x", h)[:8] } // NewRegistry creates a new empty registry func NewRegistry(filePath string) *Registry { return &Registry{ filePath: filePath, workspaces: make(map[string]Workspace), } } // Load reads the registry from a JSON file func Load(filePath string) mo.Result[*Registry] { registry := NewRegistry(filePath) if _, err := os.Stat(filePath); os.IsNotExist(err) { return mo.Ok(registry) } data, err := os.ReadFile(filePath) if err != nil { return mo.Err[*Registry](fmt.Errorf("failed to read workspaces file: %w", err)) } if len(data) == 0 { return mo.Ok(registry) } var workspaceList []Workspace if err := json.Unmarshal(data, &workspaceList); err != nil { return mo.Err[*Registry](fmt.Errorf("failed to parse workspaces JSON: %w", err)) } for _, ws := range workspaceList { registry.workspaces[ws.HostPath] = ws } return mo.Ok(registry) } // Save writes the registry to the JSON file func (r *Registry) Save() mo.Result[struct{}] { dir := filepath.Dir(r.filePath) if err := os.MkdirAll(dir, 0755); err != nil { return mo.Err[struct{}](fmt.Errorf("failed to create directory: %w", err)) } workspaceList := make([]Workspace, 0, len(r.workspaces)) for _, ws := range r.workspaces { workspaceList = append(workspaceList, ws) } data, err := json.MarshalIndent(workspaceList, "", " ") if err != nil { return mo.Err[struct{}](fmt.Errorf("failed to marshal JSON: %w", err)) } if err := os.WriteFile(r.filePath, data, 0644); err != nil { return mo.Err[struct{}](fmt.Errorf("failed to write workspaces file: %w", err)) } return mo.Ok(struct{}{}) } // Register adds a workspace to the registry if it doesn't already exist func (r *Registry) Register(hostPath string) mo.Result[*Workspace] { absPath, err := filepath.Abs(hostPath) if err != nil { return mo.Err[*Workspace](fmt.Errorf("failed to resolve absolute path: %w", err)) } if existing, exists := r.workspaces[absPath]; exists { return mo.Ok(&existing) } hash := Hash(absPath) dirName := filepath.Base(absPath) ws := Workspace{ HostPath: absPath, Hash: hash, MountTag: fmt.Sprintf("ws_%s", hash), GuestPath: fmt.Sprintf("/workspace/%s-%s", hash, dirName), } r.workspaces[absPath] = ws return mo.Ok(&ws) } // List returns all registered workspaces func (r *Registry) List() []Workspace { result := make([]Workspace, 0, len(r.workspaces)) for _, ws := range r.workspaces { result = append(result, ws) } return result } // Find looks up a workspace by host path func (r *Registry) Find(hostPath string) mo.Option[Workspace] { absPath, err := filepath.Abs(hostPath) if err != nil { if ws, exists := r.workspaces[hostPath]; exists { return mo.Some(ws) } return mo.None[Workspace]() } if ws, exists := r.workspaces[absPath]; exists { return mo.Some(ws) } return mo.None[Workspace]() } // FindByHash looks up a workspace by its hash (or hash prefix) func (r *Registry) FindByHash(hash string) mo.Option[Workspace] { for _, ws := range r.workspaces { if ws.Hash == hash { return mo.Some(ws) } // Also support prefix matching for convenience if len(hash) >= 3 && len(hash) < 8 && ws.Hash[:len(hash)] == hash { return mo.Some(ws) } } return mo.None[Workspace]() } // Unregister removes a workspace from the registry by host path func (r *Registry) Unregister(hostPath string) bool { absPath, err := filepath.Abs(hostPath) if err != nil { absPath = hostPath } if _, exists := r.workspaces[absPath]; exists { delete(r.workspaces, absPath) return true } return false } // UnregisterByHash removes a workspace from the registry by its hash func (r *Registry) UnregisterByHash(hash string) mo.Option[Workspace] { wsOpt := r.FindByHash(hash) if wsOpt.IsAbsent() { return mo.None[Workspace]() } ws := wsOpt.MustGet() delete(r.workspaces, ws.HostPath) return mo.Some(ws) }