enact_config/
project_def.rs1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6
7use crate::config::MemoryConfig;
8use crate::home::enact_home;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ProjectDef {
13 pub name: String,
14 pub slug: String,
15 #[serde(default)]
17 pub repo: Option<String>,
18 #[serde(default)]
19 pub default_agent: Option<String>,
20 #[serde(default)]
21 pub agents: Vec<String>,
22 #[serde(default)]
23 pub description: Option<String>,
24 #[serde(default)]
26 pub memory: Option<MemoryConfig>,
27}
28
29impl ProjectDef {
30 pub fn project_dir(home: &Path, slug: &str) -> PathBuf {
32 home.join("projects").join(slug)
33 }
34
35 pub fn project_yaml_path(home: &Path, slug: &str) -> PathBuf {
37 Self::project_dir(home, slug).join("project.yaml")
38 }
39
40 pub fn taskboard_path(home: &Path, slug: &str) -> PathBuf {
42 Self::project_dir(home, slug).join("taskboard.yaml")
43 }
44
45 pub fn sessions_dir(home: &Path, slug: &str) -> PathBuf {
47 Self::project_dir(home, slug).join("sessions")
48 }
49
50 pub fn context_dir(home: &Path, slug: &str) -> PathBuf {
52 Self::project_dir(home, slug).join("context")
53 }
54
55 pub fn load(home: &Path, slug: &str) -> Result<Option<Self>> {
57 let path = Self::project_yaml_path(home, slug);
58 if !path.exists() {
59 return Ok(None);
60 }
61 let s = std::fs::read_to_string(&path).context("Failed to read project.yaml")?;
62 let def: ProjectDef = serde_yaml::from_str(&s).context("Failed to parse project.yaml")?;
63 Ok(Some(def))
64 }
65
66 pub fn save(&self, home: &Path) -> Result<()> {
68 let dir = Self::project_dir(home, &self.slug);
69 std::fs::create_dir_all(&dir).context("Failed to create project directory")?;
70 let path = dir.join("project.yaml");
71 let s = serde_yaml::to_string(self).context("Failed to serialize project to YAML")?;
72 std::fs::write(&path, s).context("Failed to write project.yaml")?;
73 Ok(())
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79pub struct Task {
80 pub id: String,
81 pub title: String,
82 #[serde(default)]
83 pub description: Option<String>,
84 #[serde(default)]
85 pub priority: Option<String>,
86 #[serde(default)]
87 pub assigned_to: Option<String>,
88 #[serde(default)]
89 pub created_at: Option<String>,
90 #[serde(default)]
91 pub started_at: Option<String>,
92 #[serde(default)]
93 pub completed_at: Option<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98pub struct TaskBoard {
99 #[serde(default = "default_taskboard_version")]
100 pub version: String,
101 #[serde(default)]
102 pub updated_at: Option<String>,
103 #[serde(default)]
104 pub todo: Vec<Task>,
105 #[serde(default)]
106 pub in_progress: Vec<Task>,
107 #[serde(default)]
108 pub done: Vec<Task>,
109}
110
111fn default_taskboard_version() -> String {
112 "1.0.0".to_string()
113}
114
115impl Default for TaskBoard {
116 fn default() -> Self {
117 Self {
118 version: default_taskboard_version(),
119 updated_at: None,
120 todo: Vec::new(),
121 in_progress: Vec::new(),
122 done: Vec::new(),
123 }
124 }
125}
126
127impl TaskBoard {
128 pub fn load(home: &Path, slug: &str) -> Result<Self> {
130 let path = ProjectDef::taskboard_path(home, slug);
131 if !path.exists() {
132 return Ok(Self::default());
133 }
134 let s = std::fs::read_to_string(&path).context("Failed to read taskboard.yaml")?;
135 let board: TaskBoard =
136 serde_yaml::from_str(&s).context("Failed to parse taskboard.yaml")?;
137 Ok(board)
138 }
139
140 pub fn save(&self, home: &Path, slug: &str) -> Result<()> {
142 let dir = ProjectDef::project_dir(home, slug);
143 std::fs::create_dir_all(&dir).context("Failed to create project directory")?;
144 let path = dir.join("taskboard.yaml");
145 let s = serde_yaml::to_string(self).context("Failed to serialize taskboard to YAML")?;
146 std::fs::write(&path, s).context("Failed to write taskboard.yaml")?;
147 Ok(())
148 }
149}
150
151pub struct ProjectRegistry;
153
154impl ProjectRegistry {
155 pub fn list(home: &Path) -> Result<Vec<String>> {
157 let projects_dir = home.join("projects");
158 if !projects_dir.exists() {
159 return Ok(Vec::new());
160 }
161 let mut slugs = Vec::new();
162 for e in std::fs::read_dir(projects_dir).context("Failed to read projects directory")? {
163 let e = e?;
164 let path = e.path();
165 if path.is_dir() && path.join("project.yaml").exists() {
166 if let Some(slug) = path.file_name().and_then(|n| n.to_str()) {
167 slugs.push(slug.to_string());
168 }
169 }
170 }
171 slugs.sort();
172 Ok(slugs)
173 }
174
175 pub fn get(home: &Path, slug: &str) -> Result<Option<ProjectDef>> {
177 ProjectDef::load(home, slug)
178 }
179
180 pub fn get_default(slug: &str) -> Result<Option<ProjectDef>> {
182 Self::get(&enact_home(), slug)
183 }
184
185 pub fn get_with_taskboard(home: &Path, slug: &str) -> Result<Option<(ProjectDef, TaskBoard)>> {
187 match ProjectDef::load(home, slug)? {
188 Some(def) => {
189 let board = TaskBoard::load(home, slug)?;
190 Ok(Some((def, board)))
191 }
192 None => Ok(None),
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn project_dir_path() {
203 let home = Path::new("/tmp/.enact");
204 assert_eq!(
205 ProjectDef::project_yaml_path(home, "enact-agent"),
206 PathBuf::from("/tmp/.enact/projects/enact-agent/project.yaml")
207 );
208 }
209}