Skip to main content

systemprompt_loader/
profile_loader.rs

1//! Reads, validates, and writes profile YAML files (with embedded
2//! gateway / cloud catalogues).
3//!
4//! [`ProfileLoader`] is a thin shim over
5//! [`systemprompt_config::load_profile_with_catalog`] that adds:
6//!
7//! - on-disk path conventions (`profiles/<name>.secrets.profile.yaml`),
8//! - serialization with a leading "do not commit secrets" header, and
9//! - directory enumeration for the `systemprompt cloud` CLI commands.
10
11use std::path::Path;
12use systemprompt_config::load_profile_with_catalog;
13use systemprompt_models::Profile;
14
15use crate::error::{ProfileLoadError, ProfileLoadResult};
16
17#[derive(Debug, Clone, Copy)]
18pub struct ProfileLoader;
19
20impl ProfileLoader {
21    pub fn load_from_path(profile_path: &Path) -> ProfileLoadResult<Profile> {
22        load_profile_with_catalog(profile_path).map_err(ProfileLoadError::from)
23    }
24
25    pub fn load(services_path: &Path, profile_name: &str) -> ProfileLoadResult<Profile> {
26        let profile_path = services_path
27            .join("profiles")
28            .join(format!("{profile_name}.secrets.profile.yaml"));
29
30        Self::load_from_path(&profile_path)
31    }
32
33    pub fn load_from_path_and_validate(profile_path: &Path) -> ProfileLoadResult<Profile> {
34        let profile = Self::load_from_path(profile_path)?;
35        profile.validate().map_err(ProfileLoadError::from)?;
36        Ok(profile)
37    }
38
39    pub fn load_and_validate(
40        services_path: &Path,
41        profile_name: &str,
42    ) -> ProfileLoadResult<Profile> {
43        let profile = Self::load(services_path, profile_name)?;
44        profile.validate().map_err(ProfileLoadError::from)?;
45        Ok(profile)
46    }
47
48    pub fn save(profile: &Profile, services_path: &Path) -> ProfileLoadResult<()> {
49        let profiles_dir = services_path.join("profiles");
50        std::fs::create_dir_all(&profiles_dir).map_err(|e| ProfileLoadError::Io {
51            path: profiles_dir.clone(),
52            source: e,
53        })?;
54
55        let profile_path = profiles_dir.join(format!("{}.secrets.profile.yaml", profile.name));
56        let content = profile.to_yaml().map_err(ProfileLoadError::from)?;
57
58        let content_with_header = format!(
59            "# systemprompt.io Profile: {}\n# \n# WARNING: This file contains secrets.\n# DO NOT \
60             commit to version control.\n\n{content}",
61            profile.display_name
62        );
63
64        std::fs::write(&profile_path, content_with_header).map_err(|e| ProfileLoadError::Io {
65            path: profile_path,
66            source: e,
67        })
68    }
69
70    #[must_use]
71    pub fn list_available(services_path: &Path) -> Vec<String> {
72        let profiles_dir = services_path.join("profiles");
73
74        if !profiles_dir.exists() {
75            return Vec::new();
76        }
77
78        match std::fs::read_dir(&profiles_dir) {
79            Ok(entries) => entries
80                .filter_map(Result::ok)
81                .filter_map(|e| {
82                    let name = e.file_name().to_string_lossy().to_string();
83                    name.strip_suffix(".secrets.profile.yaml")
84                        .map(ToString::to_string)
85                })
86                .collect(),
87            Err(e) => {
88                tracing::warn!(
89                    error = %e,
90                    path = %profiles_dir.display(),
91                    "Failed to read profiles directory"
92                );
93                Vec::new()
94            },
95        }
96    }
97}