//! [`OpenProject`] and [`ListProjects`] (ARCHITECTURE ยง6). use std::sync::Arc; use domain::ports::{FileSystem, ProjectStore, RemotePath}; use domain::{AgentManifest, Project, ProjectId}; use crate::error::AppError; use super::meta::{from_json_bytes, join_root, ProjectMeta, AGENTS_FILE, IDEAI_DIR, PROJECT_FILE}; /// Input for [`OpenProject::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OpenProjectInput { /// Id of the project to open (as known by the registry). pub project_id: ProjectId, } /// Output of [`OpenProject::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OpenProjectOutput { /// The opened project (registry source of truth for id/name/root/remote). pub project: Project, /// The project-local meta read from `.ideai/project.json`, if present and /// readable. Read tolerantly: a missing/corrupt file does not fail the open. pub meta: Option, /// The agent manifest read from `.ideai/agents.json`, if present. Tolerant: /// absent in a brand-new project. pub manifest: Option, } /// Loads a project, its `.ideai/project.json` meta and (tolerantly) its manifest. pub struct OpenProject { store: Arc, fs: Arc, } impl OpenProject { /// Builds the use case from its injected ports. #[must_use] pub fn new(store: Arc, fs: Arc) -> Self { Self { store, fs } } /// Executes the open. /// /// # Errors /// - [`AppError::NotFound`] if the registry has no such project, /// - [`AppError::Store`] on registry I/O failure. /// /// Reading the `.ideai/` files is **tolerant**: their absence or a parse /// failure yields `None` rather than an error, so a project whose `.ideai/` /// was deleted can still be opened. pub async fn execute(&self, input: OpenProjectInput) -> Result { let project = self.store.load_project(input.project_id).await?; let meta = self .read_optional_json::(&project, PROJECT_FILE) .await; let manifest = self .read_optional_json::(&project, AGENTS_FILE) .await; Ok(OpenProjectOutput { project, meta, manifest, }) } /// Reads and parses a JSON file inside the project's `.ideai/`, tolerating /// any failure (missing file, I/O error, parse error) by returning `None`. async fn read_optional_json serde::Deserialize<'de>>( &self, project: &Project, file: &str, ) -> Option { let path = RemotePath::new(join_root(&project.root, &format!("{IDEAI_DIR}/{file}"))); match self.fs.read(&path).await { Ok(bytes) => from_json_bytes::(&bytes).ok(), Err(_) => None, } } } /// Output of [`ListProjects::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListProjectsOutput { /// All projects known to the registry. pub projects: Vec, } /// Lists the projects known to the registry. pub struct ListProjects { store: Arc, } impl ListProjects { /// Builds the use case from its injected port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Executes the listing. /// /// # Errors /// [`AppError::Store`] on registry I/O failure. pub async fn execute(&self) -> Result { let projects = self.store.list_projects().await?; Ok(ListProjectsOutput { projects }) } }