projects.go

  1package projects
  2
  3import (
  4	"encoding/json"
  5	"os"
  6	"path/filepath"
  7	"slices"
  8	"sync"
  9	"time"
 10
 11	"github.com/charmbracelet/crush/internal/config"
 12)
 13
 14const projectsFileName = "projects.json"
 15
 16// Project represents a tracked project directory.
 17type Project struct {
 18	Path         string    `json:"path"`
 19	DataDir      string    `json:"data_dir"`
 20	LastAccessed time.Time `json:"last_accessed"`
 21}
 22
 23// ProjectList holds the list of tracked projects.
 24type ProjectList struct {
 25	Projects []Project `json:"projects"`
 26}
 27
 28var mu sync.Mutex
 29
 30// projectsFilePath returns the path to the projects.json file.
 31func projectsFilePath() string {
 32	return filepath.Join(filepath.Dir(config.GlobalConfigData()), projectsFileName)
 33}
 34
 35// Load reads the projects list from disk.
 36func Load() (*ProjectList, error) {
 37	mu.Lock()
 38	defer mu.Unlock()
 39
 40	path := projectsFilePath()
 41	data, err := os.ReadFile(path)
 42	if err != nil {
 43		if os.IsNotExist(err) {
 44			return &ProjectList{Projects: []Project{}}, nil
 45		}
 46		return nil, err
 47	}
 48
 49	var list ProjectList
 50	if err := json.Unmarshal(data, &list); err != nil {
 51		return nil, err
 52	}
 53
 54	return &list, nil
 55}
 56
 57// Save writes the projects list to disk.
 58func Save(list *ProjectList) error {
 59	mu.Lock()
 60	defer mu.Unlock()
 61
 62	path := projectsFilePath()
 63
 64	// Ensure directory exists
 65	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
 66		return err
 67	}
 68
 69	data, err := json.MarshalIndent(list, "", "  ")
 70	if err != nil {
 71		return err
 72	}
 73
 74	return os.WriteFile(path, data, 0o600)
 75}
 76
 77// Register adds or updates a project in the list.
 78func Register(workingDir, dataDir string) error {
 79	list, err := Load()
 80	if err != nil {
 81		return err
 82	}
 83
 84	now := time.Now().UTC()
 85
 86	// Check if project already exists
 87	found := false
 88	for i, p := range list.Projects {
 89		if p.Path == workingDir {
 90			list.Projects[i].DataDir = dataDir
 91			list.Projects[i].LastAccessed = now
 92			found = true
 93			break
 94		}
 95	}
 96
 97	if !found {
 98		list.Projects = append(list.Projects, Project{
 99			Path:         workingDir,
100			DataDir:      dataDir,
101			LastAccessed: now,
102		})
103	}
104
105	// Sort by last accessed (most recent first)
106	slices.SortFunc(list.Projects, func(a, b Project) int {
107		if a.LastAccessed.After(b.LastAccessed) {
108			return -1
109		}
110		if a.LastAccessed.Before(b.LastAccessed) {
111			return 1
112		}
113		return 0
114	})
115
116	return Save(list)
117}
118
119// List returns all tracked projects sorted by last accessed.
120func List() ([]Project, error) {
121	list, err := Load()
122	if err != nil {
123		return nil, err
124	}
125	return list.Projects, nil
126}