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            database_url: db_url,
32        }),
33        Err(reason) => ProfileEntryResult::Skip(reason),
34    }
35}
36
37fn load_database_url_from_secrets(
38    secrets_json: &PathBuf,
39    profile_name: &str,
40) -> Result<String, ProfileSkipReason> {
41    let content =
42        std::fs::read_to_string(secrets_json).map_err(|e| ProfileSkipReason::SecretsReadError {
43            path: secrets_json.clone(),
44            error: e.to_string(),
45        })?;
46
47    let secrets: serde_json::Value =
48        serde_json::from_str(&content).map_err(|e| ProfileSkipReason::SecretsParseError {
49            path: secrets_json.clone(),
50            error: e.to_string(),
51        })?;
52
53    secrets
54        .get("database_url")
55        .and_then(|v| v.as_str())
56        .map(String::from)
57        .ok_or_else(|| ProfileSkipReason::MissingDatabaseUrl {
58            profile: profile_name.to_string(),
59        })
60}
61
62pub fn discover_profiles() -> Result<ProfileDiscoveryResult> {
63    let ctx = ProjectContext::discover();
64    let profiles_dir = ctx.profiles_dir();
65
66    if !profiles_dir.exists() {
67        return Ok(ProfileDiscoveryResult {
68            profiles: Vec::new(),
69            skipped: Vec::new(),
70        });
71    }
72
73    let mut profiles = Vec::new();
74    let mut skipped = Vec::new();
75
76    for entry in std::fs::read_dir(&profiles_dir)? {
77        match process_profile_entry(&ctx, entry?.path()) {
78            ProfileEntryResult::Valid(info) => profiles.push(info),
79            ProfileEntryResult::Skip(reason) => skipped.push(reason),
80            ProfileEntryResult::NotDirectory => {},
81        }
82    }
83
84    Ok(ProfileDiscoveryResult { profiles, skipped })
85}
86
87pub fn print_discovery_summary(result: &ProfileDiscoveryResult, verbose: bool) {
88    let found_count = result.profiles.len();
89    let skipped_count = result.skipped.len();
90
91    if found_count > 0 {
92        CliService::info(&format!(
93            "Found {} profile(s) with database configuration",
94            found_count
95        ));
96    }
97
98    if skipped_count > 0 {
99        print_skipped_profiles(&result.skipped, verbose, skipped_count);
100    }
101}
102
103fn print_skipped_profiles(skipped: &[ProfileSkipReason], verbose: bool, count: usize) {
104    if verbose {
105        CliService::warning(&format!("Skipped {} profile(s):", count));
106        for reason in skipped {
107            print_skip_reason(reason);
108        }
109    } else {
110        CliService::info(&format!(
111            "Skipped {} profile(s) (use -v for details)",
112            count
113        ));
114    }
115}
116
117fn print_skip_reason(reason: &ProfileSkipReason) {
118    match reason {
119        ProfileSkipReason::MissingConfig { path } => {
120            CliService::warning(&format!("  - Missing config: {}", path.display()));
121        },
122        ProfileSkipReason::MissingSecrets { path } => {
123            CliService::warning(&format!("  - Missing secrets: {}", path.display()));
124        },
125        ProfileSkipReason::SecretsReadError { path, error } => {
126            CliService::warning(&format!("  - Cannot read {}: {}", path.display(), error));
127        },
128        ProfileSkipReason::SecretsParseError { path, error } => {
129            CliService::warning(&format!(
130                "  - Invalid JSON in {}: {}",
131                path.display(),
132                error
133            ));
134        },
135        ProfileSkipReason::MissingDatabaseUrl { profile } => {
136            CliService::warning(&format!("  - No database_url in profile '{}'", profile));
137        },
138        ProfileSkipReason::InvalidDirectoryName { path } => {
139            CliService::warning(&format!("  - Invalid directory name: {}", path.display()));
140        },
141    }
142}