use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::config::MemoryConfig;
use crate::home::enact_home;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectDef {
pub name: String,
pub slug: String,
#[serde(default)]
pub repo: Option<String>,
#[serde(default)]
pub default_agent: Option<String>,
#[serde(default)]
pub agents: Vec<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub memory: Option<MemoryConfig>,
}
impl ProjectDef {
pub fn project_dir(home: &Path, slug: &str) -> PathBuf {
home.join("projects").join(slug)
}
pub fn project_yaml_path(home: &Path, slug: &str) -> PathBuf {
Self::project_dir(home, slug).join("project.yaml")
}
pub fn taskboard_path(home: &Path, slug: &str) -> PathBuf {
Self::project_dir(home, slug).join("taskboard.yaml")
}
pub fn sessions_dir(home: &Path, slug: &str) -> PathBuf {
Self::project_dir(home, slug).join("sessions")
}
pub fn context_dir(home: &Path, slug: &str) -> PathBuf {
Self::project_dir(home, slug).join("context")
}
pub fn load(home: &Path, slug: &str) -> Result<Option<Self>> {
let path = Self::project_yaml_path(home, slug);
if !path.exists() {
return Ok(None);
}
let s = std::fs::read_to_string(&path).context("Failed to read project.yaml")?;
let def: ProjectDef = serde_yaml::from_str(&s).context("Failed to parse project.yaml")?;
Ok(Some(def))
}
pub fn save(&self, home: &Path) -> Result<()> {
let dir = Self::project_dir(home, &self.slug);
std::fs::create_dir_all(&dir).context("Failed to create project directory")?;
let path = dir.join("project.yaml");
let s = serde_yaml::to_string(self).context("Failed to serialize project to YAML")?;
std::fs::write(&path, s).context("Failed to write project.yaml")?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Task {
pub id: String,
pub title: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub priority: Option<String>,
#[serde(default)]
pub assigned_to: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub started_at: Option<String>,
#[serde(default)]
pub completed_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskBoard {
#[serde(default = "default_taskboard_version")]
pub version: String,
#[serde(default)]
pub updated_at: Option<String>,
#[serde(default)]
pub todo: Vec<Task>,
#[serde(default)]
pub in_progress: Vec<Task>,
#[serde(default)]
pub done: Vec<Task>,
}
fn default_taskboard_version() -> String {
"1.0.0".to_string()
}
impl Default for TaskBoard {
fn default() -> Self {
Self {
version: default_taskboard_version(),
updated_at: None,
todo: Vec::new(),
in_progress: Vec::new(),
done: Vec::new(),
}
}
}
impl TaskBoard {
pub fn load(home: &Path, slug: &str) -> Result<Self> {
let path = ProjectDef::taskboard_path(home, slug);
if !path.exists() {
return Ok(Self::default());
}
let s = std::fs::read_to_string(&path).context("Failed to read taskboard.yaml")?;
let board: TaskBoard =
serde_yaml::from_str(&s).context("Failed to parse taskboard.yaml")?;
Ok(board)
}
pub fn save(&self, home: &Path, slug: &str) -> Result<()> {
let dir = ProjectDef::project_dir(home, slug);
std::fs::create_dir_all(&dir).context("Failed to create project directory")?;
let path = dir.join("taskboard.yaml");
let s = serde_yaml::to_string(self).context("Failed to serialize taskboard to YAML")?;
std::fs::write(&path, s).context("Failed to write taskboard.yaml")?;
Ok(())
}
}
pub struct ProjectRegistry;
impl ProjectRegistry {
pub fn list(home: &Path) -> Result<Vec<String>> {
let projects_dir = home.join("projects");
if !projects_dir.exists() {
return Ok(Vec::new());
}
let mut slugs = Vec::new();
for e in std::fs::read_dir(projects_dir).context("Failed to read projects directory")? {
let e = e?;
let path = e.path();
if path.is_dir() && path.join("project.yaml").exists() {
if let Some(slug) = path.file_name().and_then(|n| n.to_str()) {
slugs.push(slug.to_string());
}
}
}
slugs.sort();
Ok(slugs)
}
pub fn get(home: &Path, slug: &str) -> Result<Option<ProjectDef>> {
ProjectDef::load(home, slug)
}
pub fn get_default(slug: &str) -> Result<Option<ProjectDef>> {
Self::get(&enact_home(), slug)
}
pub fn get_with_taskboard(home: &Path, slug: &str) -> Result<Option<(ProjectDef, TaskBoard)>> {
match ProjectDef::load(home, slug)? {
Some(def) => {
let board = TaskBoard::load(home, slug)?;
Ok(Some((def, board)))
}
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_dir_path() {
let home = Path::new("/tmp/.enact");
assert_eq!(
ProjectDef::project_yaml_path(home, "enact-agent"),
PathBuf::from("/tmp/.enact/projects/enact-agent/project.yaml")
);
}
}