systemprompt_cli/commands/cloud/sync/admin_user/
discovery.rs1use 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}