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:
172
crates/infrastructure/src/store/context.rs
Normal file
172
crates/infrastructure/src/store/context.rs
Normal file
@ -0,0 +1,172 @@
|
||||
//! [`IdeaiContextStore`] — file implementation of the [`AgentContextStore`] port
|
||||
//! (ARCHITECTURE §5, §9.1).
|
||||
//!
|
||||
//! Persists, **inside the project root**, the agent contexts and the manifest
|
||||
//! that together make a project's agents travel with the code:
|
||||
//!
|
||||
//! ```text
|
||||
//! <project_root>/
|
||||
//! └── .ideai/
|
||||
//! ├── agents.json # AgentManifest { version, agents: [ManifestEntry, ...] }
|
||||
//! └── agents/
|
||||
//! ├── backend-dev.md # an agent's context (md_path = "agents/backend-dev.md")
|
||||
//! └── ...
|
||||
//! ```
|
||||
//!
|
||||
//! All I/O goes through the [`FileSystem`] port (here the `RemoteHost`'s
|
||||
//! filesystem — [`crate::LocalFileSystem`] locally, an SFTP/WSL one later), so the
|
||||
//! store is **location-neutral**: the very same adapter serves a local project or
|
||||
//! a project hosted over SSH/WSL (Liskov, ARCHITECTURE §1.2).
|
||||
//!
|
||||
//! `md_path` values are interpreted **relative to `.ideai/`** (so a manifest
|
||||
//! `md_path` of `agents/backend-dev.md` maps to
|
||||
//! `<root>/.ideai/agents/backend-dev.md`), matching the documented schema.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use domain::agent::AgentManifest;
|
||||
use domain::ids::AgentId;
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::ports::{AgentContextStore, FileSystem, FsError, RemotePath, StoreError};
|
||||
use domain::project::{Project, ProjectPath};
|
||||
|
||||
/// The `.ideai/` directory name inside a project root.
|
||||
const IDEAI_DIR: &str = ".ideai";
|
||||
|
||||
/// The agent-manifest file name inside `.ideai/`.
|
||||
const AGENTS_FILE: &str = "agents.json";
|
||||
|
||||
/// Current schema version written into a freshly-created manifest.
|
||||
const MANIFEST_VERSION: u32 = 1;
|
||||
|
||||
/// File-backed [`AgentContextStore`], composing a [`FileSystem`] port.
|
||||
///
|
||||
/// Cheap to clone (everything is behind `Arc`); the composition root constructs
|
||||
/// one per resolved `RemoteHost` and shares it across the agent use cases.
|
||||
#[derive(Clone)]
|
||||
pub struct IdeaiContextStore {
|
||||
fs: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl IdeaiContextStore {
|
||||
/// Builds the store from an injected [`FileSystem`] (the project's
|
||||
/// `RemoteHost` filesystem).
|
||||
#[must_use]
|
||||
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
|
||||
Self { fs }
|
||||
}
|
||||
|
||||
/// Joins a project root with a relative segment using a POSIX separator
|
||||
/// (valid on every target; `tokio::fs` accepts `/` on Windows too).
|
||||
fn join(root: &ProjectPath, rel: &str) -> String {
|
||||
let base = root.as_str().trim_end_matches(['/', '\\']);
|
||||
format!("{base}/{rel}")
|
||||
}
|
||||
|
||||
/// Absolute path of the manifest file for a project.
|
||||
fn manifest_path(project: &Project) -> RemotePath {
|
||||
RemotePath::new(Self::join(&project.root, &format!("{IDEAI_DIR}/{AGENTS_FILE}")))
|
||||
}
|
||||
|
||||
/// Absolute path of an agent context `.md` from its (`.ideai/`-relative)
|
||||
/// `md_path`.
|
||||
fn context_path(project: &Project, md_path: &str) -> RemotePath {
|
||||
RemotePath::new(Self::join(&project.root, &format!("{IDEAI_DIR}/{md_path}")))
|
||||
}
|
||||
|
||||
/// Resolves the `md_path` of an agent from the manifest, or
|
||||
/// [`StoreError::NotFound`] if the agent is unknown to this project.
|
||||
async fn md_path_of(&self, project: &Project, agent: &AgentId) -> Result<String, StoreError> {
|
||||
let manifest = self.load_manifest(project).await?;
|
||||
manifest
|
||||
.entries
|
||||
.into_iter()
|
||||
.find(|e| &e.agent_id == agent)
|
||||
.map(|e| e.md_path)
|
||||
.ok_or(StoreError::NotFound)
|
||||
}
|
||||
|
||||
/// Ensures the directory holding `path` exists (creates `.ideai/` and any
|
||||
/// nested `agents/` parent before a write).
|
||||
async fn ensure_parent(&self, path: &RemotePath) -> Result<(), StoreError> {
|
||||
let raw = path.as_str();
|
||||
let dir = match raw.rfind(['/', '\\']) {
|
||||
Some(idx) => &raw[..idx],
|
||||
None => return Ok(()),
|
||||
};
|
||||
self.fs
|
||||
.create_dir_all(&RemotePath::new(dir.to_owned()))
|
||||
.await
|
||||
.map_err(|e| StoreError::Io(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentContextStore for IdeaiContextStore {
|
||||
async fn read_context(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent: &AgentId,
|
||||
) -> Result<MarkdownDoc, StoreError> {
|
||||
let md_path = self.md_path_of(project, agent).await?;
|
||||
let path = Self::context_path(project, &md_path);
|
||||
match self.fs.read(&path).await {
|
||||
Ok(bytes) => {
|
||||
let content = String::from_utf8(bytes)
|
||||
.map_err(|e| StoreError::Serialization(e.to_string()))?;
|
||||
Ok(MarkdownDoc::new(content))
|
||||
}
|
||||
Err(FsError::NotFound(_)) => Err(StoreError::NotFound),
|
||||
Err(e) => Err(StoreError::Io(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_context(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent: &AgentId,
|
||||
md: &MarkdownDoc,
|
||||
) -> Result<(), StoreError> {
|
||||
let md_path = self.md_path_of(project, agent).await?;
|
||||
let path = Self::context_path(project, &md_path);
|
||||
self.ensure_parent(&path).await?;
|
||||
self.fs
|
||||
.write(&path, md.as_str().as_bytes())
|
||||
.await
|
||||
.map_err(|e| StoreError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
async fn load_manifest(&self, project: &Project) -> Result<AgentManifest, StoreError> {
|
||||
let path = Self::manifest_path(project);
|
||||
match self.fs.read(&path).await {
|
||||
Ok(bytes) => {
|
||||
serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
|
||||
}
|
||||
// No manifest yet: a project simply has no agents — return the empty
|
||||
// default rather than erroring (mirrors the project-store policy).
|
||||
Err(FsError::NotFound(_)) => Ok(AgentManifest {
|
||||
version: MANIFEST_VERSION,
|
||||
entries: Vec::new(),
|
||||
}),
|
||||
Err(e) => Err(StoreError::Io(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_manifest(
|
||||
&self,
|
||||
project: &Project,
|
||||
manifest: &AgentManifest,
|
||||
) -> Result<(), StoreError> {
|
||||
let path = Self::manifest_path(project);
|
||||
self.ensure_parent(&path).await?;
|
||||
let mut bytes = serde_json::to_vec_pretty(manifest)
|
||||
.map_err(|e| StoreError::Serialization(e.to_string()))?;
|
||||
bytes.push(b'\n');
|
||||
self.fs
|
||||
.write(&path, &bytes)
|
||||
.await
|
||||
.map_err(|e| StoreError::Io(e.to_string()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user