Charm-based CLI with JSON persistence, status/list filters, external editor editing, command-history undo/redo, and glamour markdown without AutoStyle OSC hangs. Co-authored-by: Cursor <cursoragent@cursor.com>
66 lines
1.4 KiB
Go
66 lines
1.4 KiB
Go
package project
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
)
|
|
|
|
// Store manages project directories under DataDir/projects.
|
|
type Store interface {
|
|
List() ([]Project, error)
|
|
Ensure(id string) error
|
|
}
|
|
|
|
// DirStore discovers projects as subdirectories of dataDir/projects.
|
|
type DirStore struct {
|
|
dataDir string
|
|
}
|
|
|
|
// NewDirStore creates a project store rooted at dataDir.
|
|
func NewDirStore(dataDir string) *DirStore {
|
|
return &DirStore{dataDir: dataDir}
|
|
}
|
|
|
|
func (s *DirStore) projectsRoot() string {
|
|
return filepath.Join(s.dataDir, "projects")
|
|
}
|
|
|
|
// List returns all project directories, sorted by name.
|
|
func (s *DirStore) List() ([]Project, error) {
|
|
root := s.projectsRoot()
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return []Project{}, nil
|
|
}
|
|
return nil, fmt.Errorf("list projects: %w", err)
|
|
}
|
|
|
|
var projects []Project
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
name := e.Name()
|
|
projects = append(projects, Project{ID: name, Name: name})
|
|
}
|
|
sort.Slice(projects, func(i, j int) bool {
|
|
return projects[i].Name < projects[j].Name
|
|
})
|
|
return projects, nil
|
|
}
|
|
|
|
// Ensure creates the project directory if it does not exist.
|
|
func (s *DirStore) Ensure(id string) error {
|
|
if id == "" {
|
|
return fmt.Errorf("project id is required")
|
|
}
|
|
dir := filepath.Join(s.projectsRoot(), id)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("create project %s: %w", id, err)
|
|
}
|
|
return nil
|
|
}
|