//! 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::ports::{AgentRuntime, ProfileStore}; use domain::profile::AgentProfile; use crate::error::AppError; use super::catalogue::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 results = input .candidates .into_iter() .map(|profile| { let available = self.runtime.detect(&profile).unwrap_or(false); ProfileAvailability { profile, available } }) .collect(); 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, } /// 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(), }) } }