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