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