Skip to main content

systemprompt_loader/
profile_loader.rs

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