feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
120
crates/application/src/project/create.rs
Normal file
120
crates/application/src/project/create.rs
Normal file
@ -0,0 +1,120 @@
|
||||
//! [`CreateProject`] — create a project from a project root (ARCHITECTURE §6).
|
||||
//!
|
||||
//! Responsibilities (and *only* these):
|
||||
//! 1. validate the root (absolute path — enforced by [`ProjectPath`]) and name,
|
||||
//! 2. enforce the cross-aggregate uniqueness invariant `(remote, root)` — this
|
||||
//! lives here, not in the domain, because it requires knowledge of *all*
|
||||
//! projects (a repository concern, ARCHITECTURE §3.2),
|
||||
//! 3. create the `.ideai/` directory and write `project.json`,
|
||||
//! 4. register the project via the [`ProjectStore`],
|
||||
//! 5. publish [`DomainEvent::ProjectCreated`].
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{Clock, EventBus, FileSystem, IdGenerator, ProjectStore, RemotePath};
|
||||
use domain::{DomainEvent, Project, ProjectId, ProjectPath, RemoteRef};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::meta::{join_root, to_json_bytes, ProjectMeta, IDEAI_DIR, PROJECT_FILE};
|
||||
|
||||
/// Input for [`CreateProject::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateProjectInput {
|
||||
/// Display name of the project.
|
||||
pub name: String,
|
||||
/// Absolute project root (validated into a [`ProjectPath`]).
|
||||
pub root: String,
|
||||
/// Where the project lives. Defaults to [`RemoteRef::Local`] when `None`.
|
||||
pub remote: Option<RemoteRef>,
|
||||
/// Default agent profile id, if already chosen.
|
||||
pub default_profile_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Output of [`CreateProject::execute`]: the freshly-created project.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateProjectOutput {
|
||||
/// The created project.
|
||||
pub project: Project,
|
||||
}
|
||||
|
||||
/// Creates a project: inits `.ideai/`, writes `project.json`, registers it.
|
||||
pub struct CreateProject {
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
clock: Arc<dyn Clock>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl CreateProject {
|
||||
/// Builds the use case from its injected ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
clock: Arc<dyn Clock>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
fs,
|
||||
ids,
|
||||
clock,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes project creation.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::Invalid`] if the name is empty or the root is not absolute,
|
||||
/// - [`AppError::Invalid`] if a project already exists for the same
|
||||
/// `(remote, root)` (uniqueness invariant),
|
||||
/// - [`AppError::FileSystem`] / [`AppError::Store`] on I/O failures.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: CreateProjectInput,
|
||||
) -> Result<CreateProjectOutput, AppError> {
|
||||
let root = ProjectPath::new(input.root).map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
let remote = input.remote.unwrap_or(RemoteRef::Local);
|
||||
|
||||
// (1+2) Uniqueness invariant on (remote, root) — repository-level.
|
||||
let existing = self.store.list_projects().await?;
|
||||
if existing
|
||||
.iter()
|
||||
.any(|p| p.remote == remote && p.root == root)
|
||||
{
|
||||
return Err(AppError::Invalid(format!(
|
||||
"a project already exists at {} for this remote",
|
||||
root.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
// Build the validated aggregate (enforces non-empty name).
|
||||
let id = ProjectId::from_uuid(self.ids.new_uuid());
|
||||
let created_at = self.clock.now_millis();
|
||||
let project = Project::new(id, input.name, root.clone(), remote, created_at)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
|
||||
// (3) Materialise `.ideai/` and write `project.json`.
|
||||
let ideai_dir = join_root(&root, IDEAI_DIR);
|
||||
self.fs.create_dir_all(&RemotePath::new(ideai_dir)).await?;
|
||||
|
||||
let meta = ProjectMeta::from_project(&project, input.default_profile_id);
|
||||
let meta_path = join_root(&root, &format!("{IDEAI_DIR}/{PROJECT_FILE}"));
|
||||
self.fs
|
||||
.write(&RemotePath::new(meta_path), &to_json_bytes(&meta)?)
|
||||
.await?;
|
||||
|
||||
// (4) Register in the known-projects registry.
|
||||
self.store.save_project(&project).await?;
|
||||
|
||||
// (5) Announce.
|
||||
self.events
|
||||
.publish(DomainEvent::ProjectCreated { project_id: id });
|
||||
|
||||
Ok(CreateProjectOutput { project })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user