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}