//! Profile use cases (ARCHITECTURE §6, L5). Each is a single-responsibility //! struct carrying its ports as `Arc` and exposing one `execute`. //! //! - [`DetectProfiles`] — probe a set of candidate profiles via [`AgentRuntime`] //! and report which CLIs are installed (first-run availability ✓/✗). //! - [`ListProfiles`] / [`SaveProfile`] / [`DeleteProfile`] — CRUD over the //! persisted profiles through the [`ProfileStore`]. //! - [`ConfigureProfiles`] — persist a batch of chosen/edited/custom profiles //! (closes the first-run wizard). //! - [`ReferenceProfiles`] — expose the pre-filled, editable catalogue. //! - [`FirstRunState`] — tell the UI whether the first-run wizard should show //! (no `profiles.json` yet) and hand it the reference catalogue. use std::sync::Arc; use domain::ids::ProfileId; use domain::ports::{AgentRuntime, IdGenerator, ProfileStore}; use domain::profile::{AgentProfile, OpenCodeConfig, StructuredAdapter}; use crate::error::AppError; use super::catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles}; // --------------------------------------------------------------------------- // DetectProfiles // --------------------------------------------------------------------------- /// Input for [`DetectProfiles::execute`]: the candidate profiles to probe. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DetectProfilesInput { /// Profiles whose `detect` command should be run. pub candidates: Vec, } /// Availability of a single candidate after detection. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProfileAvailability { /// The probed profile. pub profile: AgentProfile, /// Whether its CLI was detected as installed (exit code 0). pub available: bool, } /// Output of [`DetectProfiles::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DetectProfilesOutput { /// One entry per candidate (same order), with its availability. pub results: Vec, } /// Probes candidate profiles' detection commands and reports availability. pub struct DetectProfiles { runtime: Arc, } impl DetectProfiles { /// Builds the use case from the [`AgentRuntime`] port. The runtime itself /// holds the [`domain::ports::ProcessSpawner`] used for detection. #[must_use] pub fn new(runtime: Arc) -> Self { Self { runtime } } /// Runs detection for each candidate. A detection *error* (e.g. the command /// could not even be launched) is reported as `available: false`, not a /// hard failure — the wizard just shows ✗ and the user can still keep the /// profile. /// /// # Errors /// Currently never returns `Err` (failures degrade to `available: false`); /// the `Result` keeps the signature uniform with the other use cases. pub async fn execute( &self, input: DetectProfilesInput, ) -> Result { let mut results = Vec::with_capacity(input.candidates.len()); for profile in input.candidates { let available = self.runtime.detect(&profile).await.unwrap_or(false); results.push(ProfileAvailability { profile, available }); } Ok(DetectProfilesOutput { results }) } } // --------------------------------------------------------------------------- // ListProfiles // --------------------------------------------------------------------------- /// Output of [`ListProfiles::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListProfilesOutput { /// All configured profiles. pub profiles: Vec, } /// Lists the configured profiles from the store. pub struct ListProfiles { store: Arc, } impl ListProfiles { /// Builds the use case from the [`ProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Lists configured profiles. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self) -> Result { Ok(ListProfilesOutput { profiles: self.store.list().await?, }) } } // --------------------------------------------------------------------------- // SaveProfile // --------------------------------------------------------------------------- /// Input for [`SaveProfile::execute`]: the profile to upsert. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SaveProfileInput { /// The profile to create or replace (by id). pub profile: AgentProfile, } /// Output of [`SaveProfile::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SaveProfileOutput { /// The saved profile (echoed back). pub profile: AgentProfile, } // --------------------------------------------------------------------------- // CloneOpenCodeProfileFromSeed // --------------------------------------------------------------------------- /// Input for [`CloneOpenCodeProfileFromSeed::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CloneOpenCodeProfileFromSeedInput { /// Optional display name for the cloned profile. When absent, a copy label is /// derived from the seed name. pub name: Option, /// Optional OpenCode config override. When absent, the seed config is copied. pub opencode: Option, } /// Output of [`CloneOpenCodeProfileFromSeed::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CloneOpenCodeProfileFromSeedOutput { /// The newly persisted profile. pub profile: AgentProfile, } /// Creates a new OpenCode profile instance from the canonical seed/template. /// /// The persisted canonical `opencode-llamacpp` profile is preferred when present, /// so local edits are preserved as the clone template. If it is absent, the /// in-memory reference catalogue seed is used. The new profile always receives a /// fresh [`ProfileId`], which is the only identity constraint; multiple profiles /// with `StructuredAdapter::OpenCode` are therefore valid. pub struct CloneOpenCodeProfileFromSeed { store: Arc, ids: Arc, } impl CloneOpenCodeProfileFromSeed { /// Builds the use case from the profile store and id generator ports. #[must_use] pub fn new(store: Arc, ids: Arc) -> Self { Self { store, ids } } /// Clones the canonical OpenCode seed into a new persisted profile. /// /// # Errors /// [`AppError::Store`] on persistence failure, [`AppError::Invalid`] if the /// requested name is blank, or [`AppError::Internal`] if the seed is malformed. pub async fn execute( &self, input: CloneOpenCodeProfileFromSeedInput, ) -> Result { let existing = self.store.list().await?; let seed_id = reference_profile_id("opencode-llamacpp"); let seed = existing .iter() .find(|profile| profile.id == seed_id) .cloned() .or_else(|| { reference_profiles() .into_iter() .find(|profile| profile.id == seed_id) }) .ok_or_else(|| { AppError::Internal("canonical OpenCode seed `opencode-llamacpp` is missing".into()) })?; if seed.structured_adapter != Some(StructuredAdapter::OpenCode) || seed.opencode.is_none() { return Err(AppError::Internal( "canonical OpenCode seed is not an OpenCode profile".into(), )); } let mut profile = seed; profile.id = fresh_profile_id(&*self.ids, &existing)?; profile.name = match input.name { Some(name) => { if name.trim().is_empty() { return Err(AppError::Invalid("profile.name must not be empty".into())); } name } None => format!("{} copy", profile.name), }; if let Some(config) = input.opencode { profile.opencode = Some(config); } self.store.save(&profile).await?; Ok(CloneOpenCodeProfileFromSeedOutput { profile }) } } fn fresh_profile_id( ids: &dyn IdGenerator, existing: &[AgentProfile], ) -> Result { for _ in 0..16 { let id = ProfileId::from_uuid(ids.new_uuid()); if existing.iter().all(|profile| profile.id != id) { return Ok(id); } } Err(AppError::Internal( "could not allocate a unique profile id".into(), )) } /// Persists (creates or replaces) a single profile. pub struct SaveProfile { store: Arc, } impl SaveProfile { /// Builds the use case from the [`ProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Saves the profile. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: SaveProfileInput) -> Result { self.store.save(&input.profile).await?; Ok(SaveProfileOutput { profile: input.profile, }) } } // --------------------------------------------------------------------------- // DeleteProfile // --------------------------------------------------------------------------- /// Input for [`DeleteProfile::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteProfileInput { /// Id of the profile to delete. pub id: domain::ids::ProfileId, } /// Deletes a profile by id. pub struct DeleteProfile { store: Arc, } impl DeleteProfile { /// Builds the use case from the [`ProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Deletes the profile. /// /// # Errors /// [`AppError::NotFound`] if the id is unknown, [`AppError::Store`] on /// persistence failure. pub async fn execute(&self, input: DeleteProfileInput) -> Result<(), AppError> { self.store.delete(input.id).await?; Ok(()) } } // --------------------------------------------------------------------------- // ConfigureProfiles // --------------------------------------------------------------------------- /// Input for [`ConfigureProfiles::execute`]: the chosen/edited/custom profiles. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConfigureProfilesInput { /// All profiles the user decided to keep (closes the first run). pub profiles: Vec, } /// Output of [`ConfigureProfiles::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConfigureProfilesOutput { /// The persisted profiles. pub profiles: Vec, } /// Persists the batch of profiles chosen at the end of the first-run wizard. /// /// Saving even an empty list creates `profiles.json`, which marks the first run /// as done (so the wizard does not reappear). pub struct ConfigureProfiles { store: Arc, } impl ConfigureProfiles { /// Builds the use case from the [`ProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Persists each chosen profile. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: ConfigureProfilesInput, ) -> Result { for profile in &input.profiles { self.store.save(profile).await?; } // Ensure `profiles.json` exists even when the user kept nothing, so the // first run is recorded as complete. if input.profiles.is_empty() { self.store.mark_configured().await?; } Ok(ConfigureProfilesOutput { profiles: input.profiles, }) } } // --------------------------------------------------------------------------- // ReferenceProfiles (catalogue accessor) // --------------------------------------------------------------------------- /// Output of [`ReferenceProfiles::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReferenceProfilesOutput { /// The pre-filled, editable reference catalogue, **restricted to the /// selectable profiles** (§17.3, D7): only profiles drivable in structured /// mode are offered to selection/creation. Today: Claude + Codex. pub profiles: Vec, } /// Exposes the **selectable** reference catalogue for the agent-creation menu /// (§17.3, D7): the structured-drivable profiles only (Claude/Codex). Gemini and /// Aider remain in the raw catalogue data but are not proposed here. #[derive(Default)] pub struct ReferenceProfiles; impl ReferenceProfiles { /// Builds the (stateless) use case. #[must_use] pub fn new() -> Self { Self } /// Returns the reference catalogue. Infallible. /// /// # Errors /// Never; the `Result` keeps the call site uniform. #[allow(clippy::unused_async)] pub async fn execute(&self) -> Result { Ok(ReferenceProfilesOutput { profiles: selectable_reference_profiles(), }) } } // --------------------------------------------------------------------------- // FirstRunState // --------------------------------------------------------------------------- /// Output of [`FirstRunState::execute`]: whether to show the wizard + catalogue. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FirstRunStateOutput { /// `true` when no `profiles.json` exists yet ⇒ show the first-run wizard. pub is_first_run: bool, /// The pre-filled reference catalogue to seed the wizard, **restricted to the /// selectable profiles** (§17.3, D7): only structured-drivable profiles /// (Claude/Codex) are offered. No custom-profile entry. pub reference_profiles: Vec, } /// Reports whether the IDE is on its first run (no profiles configured yet) and /// provides the reference catalogue to seed the wizard. pub struct FirstRunState { store: Arc, } impl FirstRunState { /// Builds the use case from the [`ProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Computes the first-run state. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self) -> Result { let configured = self.store.is_configured().await?; Ok(FirstRunStateOutput { is_first_run: !configured, reference_profiles: selectable_reference_profiles(), }) } }