feat: add phase 3 readonly models

This commit is contained in:
Yoilun
2026-05-25 18:21:02 +08:00
parent 37e3d77110
commit d573bde194
18 changed files with 964 additions and 12 deletions

View 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"`
}

View 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
}

View 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))
}
}