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 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}