Skip to main content

systemprompt_cli/commands/cloud/sync/admin_user/
discovery.rs

1use anyhow::Result;
2use std::path::PathBuf;
3use systemprompt_cloud::{ProfilePath, ProjectContext};
4use systemprompt_logging::CliService;
5
6use super::types::{ProfileDiscoveryResult, ProfileEntryResult, ProfileInfo, ProfileSkipReason};
7
8fn process_profile_entry(ctx: &ProjectContext, path: PathBuf) -> ProfileEntryResult {
9    if !path.is_dir() {
10        return ProfileEntryResult::NotDirectory;
11    }
12
13    let name = match path.file_name().and_then(|n| n.to_str()) {
14        Some(n) => n.to_string(),
15        None => return ProfileEntryResult::Skip(ProfileSkipReason::InvalidDirectoryName { path }),
16    };
17
18    let profile_yaml = ctx.profile_path(&name, ProfilePath::Config);
19    let secrets_json = ctx.profile_path(&name, ProfilePath::Secrets);
20
21    if !profile_yaml.exists() {
22        return ProfileEntryResult::Skip(ProfileSkipReason::MissingConfig { path: profile_yaml });
23    }
24    if !secrets_json.exists() {
25        return ProfileEntryResult::Skip(ProfileSkipReason::MissingSecrets { path: secrets_json });
26    }
27
28    match load_database_url_from_secrets(&secrets_json, &name) {
29        Ok(db_url) => ProfileEntryResult::Valid(ProfileInfo {
30            name,
31            display_name: None,
32            database_url: Some(db_url),
33            tenant_id: None,
34            validation_mode: None,
35            credentials_path: None,
36            routing: None,
37            is_active: None,
38            session_status: None,
39        }),
40        Err(reason) => ProfileEntryResult::Skip(reason),
41    }
42}
43
44fn load_database_url_from_secrets(
45    secrets_json: &PathBuf,
46    profile_name: &str,
47) -> Result<String, ProfileSkipReason> {
48    let content =
49        std::fs::read_to_string(secrets_json).map_err(|e| ProfileSkipReason::SecretsReadError {
50            path: secrets_json.clone(),
51            error: e.to_string(),
52        })?;
53
54    let secrets: serde_json::Value =
55        serde_json::from_str(&content).map_err(|e| ProfileSkipReason::SecretsParseError {
56            path: secrets_json.clone(),
57            error: e.to_string(),
58        })?;
59
60    secrets
61        .get("database_url")
62        .and_then(|v| v.as_str())
63        .map(String::from)
64        .ok_or_else(|| ProfileSkipReason::MissingDatabaseUrl {
65            profile: profile_name.to_string(),
66        })
67}
68
69pub fn discover_profiles() -> Result<ProfileDiscoveryResult> {
70    let ctx = ProjectContext::discover();
71    let profiles_dir = ctx.profiles_dir();
72
73    if !profiles_dir.exists() {
74        return Ok(ProfileDiscoveryResult {
75            profiles: Vec::new(),
76            skipped: Vec::new(),
77        });
78    }
79
80    let mut profiles = Vec::new();
81    let mut skipped = Vec::new();
82
83    for entry in std::fs::read_dir(&profiles_dir)? {
84        match process_profile_entry(&ctx, entry?.path()) {
85            ProfileEntryResult::Valid(info) => profiles.push(info),
86            ProfileEntryResult::Skip(reason) => skipped.push(reason),
87            ProfileEntryResult::NotDirectory => {},
88        }
89    }
90
91    Ok(ProfileDiscoveryResult { profiles, skipped })
92}
93
94pub fn print_discovery_summary(result: &ProfileDiscoveryResult, verbose: bool) {
95    let found_count = result.profiles.len();
96    let skipped_count = result.skipped.len();
97
98    if found_count > 0 {
99        CliService::info(&format!(
100            "Found {} profile(s) with database configuration",
101            found_count
102        ));
103    }
104
105    if skipped_count > 0 {
106        print_skipped_profiles(&result.skipped, verbose, skipped_count);
107    }
108}
109
110fn print_skipped_profiles(skipped: &[ProfileSkipReason], verbose: bool, count: usize) {
111    if verbose {
112        CliService::warning(&format!("Skipped {} profile(s):", count));
113        for reason in skipped {
114            print_skip_reason(reason);
115        }
116    } else {
117        CliService::info(&format!(
118            "Skipped {} profile(s) (use -v for details)",
119            count
120        ));
121    }
122}
123
124fn print_skip_reason(reason: &ProfileSkipReason) {
125    match reason {
126        ProfileSkipReason::MissingConfig { path } => {
127            CliService::warning(&format!("  - Missing config: {}", path.display()));
128        },
129        ProfileSkipReason::MissingSecrets { path } => {
130            CliService::warning(&format!("  - Missing secrets: {}", path.display()));
131        },
132        ProfileSkipReason::SecretsReadError { path, error } => {
133            CliService::warning(&format!("  - Cannot read {}: {}", path.display(), error));
134        },
135        ProfileSkipReason::SecretsParseError { path, error } => {
136            CliService::warning(&format!(
137                "  - Invalid JSON in {}: {}",
138                path.display(),
139                error
140            ));
141        },
142        ProfileSkipReason::MissingDatabaseUrl { profile } => {
143            CliService::warning(&format!("  - No database_url in profile '{}'", profile));
144        },
145        ProfileSkipReason::InvalidDirectoryName { path } => {
146            CliService::warning(&format!("  - Invalid directory name: {}", path.display()));
147        },
148    }
149}