feat: add phase 3 readonly models
This commit is contained in:
16
internal/projects/model.go
Normal file
16
internal/projects/model.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package projects
|
||||
|
||||
type Project struct {
|
||||
Path string `json:"path"`
|
||||
DisplayName string `json:"displayName"`
|
||||
TrustLevel string `json:"trustLevel"`
|
||||
DirectoryExists bool `json:"directoryExists"`
|
||||
Source SourceEvidence `json:"source"`
|
||||
}
|
||||
|
||||
type SourceEvidence struct {
|
||||
Kind string `json:"kind"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Confidence string `json:"confidence"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
92
internal/projects/store.go
Normal file
92
internal/projects/store.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codex-agent-manager/internal/codexhome"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
CodexHome string
|
||||
}
|
||||
|
||||
func (s Store) List() ([]Project, error) {
|
||||
configPath, err := codexhome.ResolveInside(s.CodexHome, "config.toml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []Project{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects := parseProjectsConfig(string(data), configPath)
|
||||
sort.Slice(projects, func(i, j int) bool {
|
||||
return projects[i].Path < projects[j].Path
|
||||
})
|
||||
for i := range projects {
|
||||
if projects[i].DisplayName == "" {
|
||||
projects[i].DisplayName = filepath.Base(projects[i].Path)
|
||||
}
|
||||
if info, err := os.Stat(projects[i].Path); err == nil && info.IsDir() {
|
||||
projects[i].DirectoryExists = true
|
||||
}
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func parseProjectsConfig(input string, sourcePath string) []Project {
|
||||
var result []Project
|
||||
var current *Project
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
current = nil
|
||||
section := strings.TrimSuffix(strings.TrimPrefix(line, "["), "]")
|
||||
if !strings.HasPrefix(section, "projects.") {
|
||||
continue
|
||||
}
|
||||
path, err := strconv.Unquote(strings.TrimPrefix(section, "projects."))
|
||||
if err != nil || path == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, Project{
|
||||
Path: path,
|
||||
Source: SourceEvidence{Kind: "config_toml", Path: sourcePath, Confidence: "high"},
|
||||
TrustLevel: "unknown",
|
||||
})
|
||||
current = &result[len(result)-1]
|
||||
continue
|
||||
}
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
key, raw, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.Unquote(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(key) {
|
||||
case "trust_level":
|
||||
current.TrustLevel = value
|
||||
case "display_name":
|
||||
current.DisplayName = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
71
internal/projects/store_test.go
Normal file
71
internal/projects/store_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStoreListsProjectsFromConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
existing := filepath.Join(root, "workspace-a")
|
||||
missing := filepath.Join(root, "workspace-b")
|
||||
if err := os.MkdirAll(existing, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config := `[projects."` + existing + `"]
|
||||
trust_level = "trusted"
|
||||
display_name = "Alpha"
|
||||
|
||||
[projects."` + missing + `"]
|
||||
trust_level = "untrusted"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(root, "config.toml"), []byte(config), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
items, err := Store{CodexHome: root}.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("len(items) = %d, want 2: %#v", len(items), items)
|
||||
}
|
||||
if items[0].Path != existing || items[0].DisplayName != "Alpha" || items[0].TrustLevel != "trusted" || !items[0].DirectoryExists {
|
||||
t.Fatalf("unexpected first project: %#v", items[0])
|
||||
}
|
||||
if items[1].Path != missing || items[1].DisplayName != filepath.Base(missing) || items[1].TrustLevel != "untrusted" || items[1].DirectoryExists {
|
||||
t.Fatalf("unexpected second project: %#v", items[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreListsProjectsInStablePathOrder(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
config := `[projects."/tmp/zeta"]
|
||||
trust_level = "trusted"
|
||||
|
||||
[projects."/tmp/alpha"]
|
||||
trust_level = "trusted"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(root, "config.toml"), []byte(config), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
items, err := Store{CodexHome: root}.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
if got, want := []string{items[0].Path, items[1].Path}, []string{"/tmp/alpha", "/tmp/zeta"}; got[0] != want[0] || got[1] != want[1] {
|
||||
t.Fatalf("paths = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreMissingConfigReturnsEmptyListWithLowConfidenceSource(t *testing.T) {
|
||||
items, err := Store{CodexHome: t.TempDir()}.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("len(items) = %d, want 0", len(items))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user