on 8ebab21 help sign in
crush/internal/projects/projects.go
126 lines 2.5 kB history raw
1package projects
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "slices"
8 "sync"
9 "time"
11 "github.com/charmbracelet/crush/internal/config"
14const projectsFileName = "projects.json"
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"`
23// ProjectList holds the list of tracked projects.
24type ProjectList struct {
25 Projects []Project `json:"projects"`
28var mu sync.Mutex
30// projectsFilePath returns the path to the projects.json file.
31func projectsFilePath() string {
32 return filepath.Join(filepath.Dir(config.GlobalConfigData()), projectsFileName)
35// Load reads the projects list from disk.
36func Load() (*ProjectList, error) {
37 mu.Lock()
38 defer mu.Unlock()
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 }
49 var list ProjectList
50 if err := json.Unmarshal(data, &list); err != nil {
51 return nil, err
52 }
54 return &list, nil
57// Save writes the projects list to disk.
58func Save(list *ProjectList) error {
59 mu.Lock()
60 defer mu.Unlock()
62 path := projectsFilePath()
64 // Ensure directory exists
65 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
66 return err
67 }
69 data, err := json.MarshalIndent(list, "", " ")
70 if err != nil {
71 return err
72 }
74 return os.WriteFile(path, data, 0o600)
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 }
84 now := time.Now().UTC()
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 }
97 if !found {
98 list.Projects = append(list.Projects, Project{
99 Path: workingDir,
100 DataDir: dataDir,
101 LastAccessed: now,
102 })
103 }
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 })
116 return Save(list)
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