systemprompt_cli/commands/cloud/deploy/
select.rs1use std::path::PathBuf;
7
8use anyhow::{Context, Result, bail};
9use systemprompt_cloud::{CloudPath, ProfilePath, ProjectContext, TenantStore, get_cloud_paths};
10use systemprompt_loader::ProfileLoader;
11use systemprompt_logging::CliService;
12use systemprompt_models::Profile;
13
14use crate::cli_settings::CliConfig;
15use crate::interactive::Prompter;
16use crate::shared::profile::{DiscoveredProfile, discover_profiles};
17
18#[derive(Debug)]
19struct DeployableProfile {
20 pub name: String,
21 pub path: PathBuf,
22 pub profile: Profile,
23 pub tenant_name: Option<String>,
24 pub hostname: Option<String>,
25}
26
27fn discover_deployable_profiles(tenant_store: &TenantStore) -> Result<Vec<DeployableProfile>> {
28 let profiles = discover_profiles()?;
29
30 let deployable = profiles
31 .into_iter()
32 .filter_map(|p| to_deployable_profile(p, tenant_store))
33 .collect();
34
35 Ok(deployable)
36}
37
38fn to_deployable_profile(
39 discovered: DiscoveredProfile,
40 tenant_store: &TenantStore,
41) -> Option<DeployableProfile> {
42 if discovered.profile.target != systemprompt_models::ProfileType::Cloud {
43 return None;
44 }
45
46 let cloud = discovered.profile.cloud.as_ref()?;
47 let tenant_id = cloud.tenant_id.as_ref()?;
48 let tenant = tenant_store.find_tenant(tenant_id);
49
50 Some(DeployableProfile {
51 name: discovered.name,
52 path: discovered.path,
53 profile: discovered.profile,
54 tenant_name: tenant.map(|t| t.name.clone()),
55 hostname: tenant.and_then(|t| t.hostname.clone()),
56 })
57}
58
59fn select_profile_interactive(
60 prompter: &dyn Prompter,
61 profiles: &[DeployableProfile],
62) -> Result<usize> {
63 let options: Vec<String> = profiles
64 .iter()
65 .map(|p| {
66 let target = p.hostname.as_deref().unwrap_or("unknown");
67 let tenant = p.tenant_name.as_deref().unwrap_or("unknown");
68 format!("{} → {} ({})", p.name, tenant, target)
69 })
70 .collect();
71
72 prompter
73 .select("Select profile to deploy", &options)
74 .context("Failed to select profile")
75}
76
77pub fn resolve_profile(
78 prompter: &dyn Prompter,
79 profile_name: Option<&str>,
80 config: &CliConfig,
81) -> Result<(Profile, PathBuf)> {
82 if let Some(name) = profile_name {
83 return resolve_profile_by_name(name);
84 }
85
86 if !config.is_interactive() {
87 bail!("--profile is required in non-interactive mode for deploy");
88 }
89
90 resolve_profile_interactive(prompter)
91}
92
93fn resolve_profile_by_name(name: &str) -> Result<(Profile, PathBuf)> {
94 let ctx = ProjectContext::discover();
95 let profile_path = ctx.profile_path(name, ProfilePath::Config);
96
97 if !profile_path.exists() {
98 bail!("Profile '{}' not found at {}", name, profile_path.display());
99 }
100
101 let profile = ProfileLoader::load_from_path(&profile_path)
102 .with_context(|| format!("Failed to load profile: {}", name))?;
103
104 Ok((profile, profile_path))
105}
106
107fn resolve_profile_interactive(prompter: &dyn Prompter) -> Result<(Profile, PathBuf)> {
108 let cloud_paths = get_cloud_paths();
109 let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
110 let tenant_store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
111 CliService::warning(&format!("Failed to load tenant store: {}", e));
112 TenantStore::default()
113 });
114
115 let profiles = discover_deployable_profiles(&tenant_store)?;
116
117 if profiles.is_empty() {
118 bail!(
119 "No deployable profiles found.\nCreate a cloud profile with: systemprompt cloud \
120 profile create <name>"
121 );
122 }
123
124 CliService::section("Select Profile");
125 let selection = select_profile_interactive(prompter, &profiles)?;
126 let selected = &profiles[selection];
127
128 Ok((selected.profile.clone(), selected.path.clone()))
129}