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