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:
149
crates/infrastructure/src/store/profile.rs
Normal file
149
crates/infrastructure/src/store/profile.rs
Normal file
@ -0,0 +1,149 @@
|
||||
//! [`FsProfileStore`] — JSON file implementation of the [`ProfileStore`] port.
|
||||
//!
|
||||
//! Persists the configured [`AgentProfile`]s in the global IDE store
|
||||
//! (ARCHITECTURE §9.2):
|
||||
//!
|
||||
//! ```text
|
||||
//! <app_data_dir>/
|
||||
//! └── profiles.json # { version, profiles: [AgentProfile, ...] }
|
||||
//! ```
|
||||
//!
|
||||
//! Each profile item is exactly the declarative profile of CONTEXT §9
|
||||
//! (`id, name, command, args, contextInjection{strategy,…}, detect, cwd`). The
|
||||
//! existence of `profiles.json` is what marks the first run as *done* (see
|
||||
//! [`ProfileStore::is_configured`]).
|
||||
//!
|
||||
//! Like [`super::FsProjectStore`], the store is Tauri-agnostic: the app-data
|
||||
//! directory is resolved by the composition root and injected as a plain path,
|
||||
//! and all I/O goes through the [`FileSystem`] port.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{FileSystem, ProfileStore, RemotePath, StoreError};
|
||||
use domain::profile::AgentProfile;
|
||||
|
||||
/// File name of the profiles store inside the app-data dir.
|
||||
const PROFILES_FILE: &str = "profiles.json";
|
||||
|
||||
/// Current schema version of the profiles file.
|
||||
const PROFILES_VERSION: u32 = 1;
|
||||
|
||||
/// On-disk shape of `profiles.json`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProfilesDoc {
|
||||
/// Schema version.
|
||||
version: u32,
|
||||
/// All configured profiles.
|
||||
profiles: Vec<AgentProfile>,
|
||||
}
|
||||
|
||||
impl Default for ProfilesDoc {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: PROFILES_VERSION,
|
||||
profiles: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON-file implementation of the [`ProfileStore`] port.
|
||||
///
|
||||
/// Cheap to clone (everything is behind `Arc`); built once at the composition
|
||||
/// root and shared across use cases.
|
||||
#[derive(Clone)]
|
||||
pub struct FsProfileStore {
|
||||
fs: Arc<dyn FileSystem>,
|
||||
app_data_dir: String,
|
||||
}
|
||||
|
||||
impl FsProfileStore {
|
||||
/// Builds the store from an injected [`FileSystem`] and the app-data
|
||||
/// directory path (resolved by the composition root). The directory is
|
||||
/// created lazily on first write.
|
||||
#[must_use]
|
||||
pub fn new(fs: Arc<dyn FileSystem>, app_data_dir: impl Into<String>) -> Self {
|
||||
Self {
|
||||
fs,
|
||||
app_data_dir: app_data_dir.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins the app-data dir with the profiles file name (POSIX separator, valid
|
||||
/// on every target — `tokio::fs` accepts `/` on Windows too).
|
||||
fn path(&self) -> RemotePath {
|
||||
let base = self.app_data_dir.trim_end_matches(['/', '\\']);
|
||||
RemotePath::new(format!("{base}/{PROFILES_FILE}"))
|
||||
}
|
||||
|
||||
/// Reads and parses the doc, returning an empty default if the file is absent.
|
||||
async fn read_doc(&self) -> Result<ProfilesDoc, StoreError> {
|
||||
match self.fs.read(&self.path()).await {
|
||||
Ok(bytes) => {
|
||||
serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
|
||||
}
|
||||
Err(domain::ports::FsError::NotFound(_)) => Ok(ProfilesDoc::default()),
|
||||
Err(e) => Err(StoreError::Io(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the doc, ensuring the app-data dir exists first.
|
||||
async fn write_doc(&self, doc: &ProfilesDoc) -> Result<(), StoreError> {
|
||||
let dir = RemotePath::new(self.app_data_dir.trim_end_matches(['/', '\\']).to_owned());
|
||||
self.fs
|
||||
.create_dir_all(&dir)
|
||||
.await
|
||||
.map_err(|e| StoreError::Io(e.to_string()))?;
|
||||
let bytes =
|
||||
serde_json::to_vec_pretty(doc).map_err(|e| StoreError::Serialization(e.to_string()))?;
|
||||
self.fs
|
||||
.write(&self.path(), &bytes)
|
||||
.await
|
||||
.map_err(|e| StoreError::Io(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ProfileStore for FsProfileStore {
|
||||
async fn list(&self) -> Result<Vec<AgentProfile>, StoreError> {
|
||||
Ok(self.read_doc().await?.profiles)
|
||||
}
|
||||
|
||||
async fn save(&self, profile: &AgentProfile) -> Result<(), StoreError> {
|
||||
let mut doc = self.read_doc().await?;
|
||||
if let Some(slot) = doc.profiles.iter_mut().find(|p| p.id == profile.id) {
|
||||
*slot = profile.clone();
|
||||
} else {
|
||||
doc.profiles.push(profile.clone());
|
||||
}
|
||||
self.write_doc(&doc).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: ProfileId) -> Result<(), StoreError> {
|
||||
let mut doc = self.read_doc().await?;
|
||||
let before = doc.profiles.len();
|
||||
doc.profiles.retain(|p| p.id != id);
|
||||
if doc.profiles.len() == before {
|
||||
return Err(StoreError::NotFound);
|
||||
}
|
||||
self.write_doc(&doc).await
|
||||
}
|
||||
|
||||
async fn is_configured(&self) -> Result<bool, StoreError> {
|
||||
self.fs
|
||||
.exists(&self.path())
|
||||
.await
|
||||
.map_err(|e| StoreError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
async fn mark_configured(&self) -> Result<(), StoreError> {
|
||||
// Write the current doc back (an empty default when nothing exists),
|
||||
// which materialises `profiles.json` and records first-run completion.
|
||||
let doc = self.read_doc().await?;
|
||||
self.write_doc(&doc).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user