Skip to main content

systemprompt_cli/commands/cloud/deploy/
select.rs

1//! Deployable-profile discovery and interactive selection for deploys.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}