Skip to main content

systemprompt_cli/commands/cloud/profile/
mod.rs

1//! `cloud profile` subcommands for managing deployment profiles.
2//!
3//! Dispatches [`ProfileCommands`] to create, list, show, edit, and delete
4//! profiles, and drives the interactive operation picker when no subcommand is
5//! given.
6
7mod api_keys;
8mod args;
9mod builders;
10mod create;
11mod create_setup;
12mod create_tenant;
13pub(super) mod delete;
14mod edit;
15mod edit_secrets;
16mod edit_settings;
17mod list;
18mod profile_steps;
19mod show;
20mod show_display;
21mod show_types;
22pub mod templates;
23
24pub use api_keys::collect_api_keys;
25pub use args::{CreateArgs, DeleteArgs, EditArgs, ProfileCommands, ShowFilter, TenantTypeArg};
26pub use create::{CreatedProfile, create_profile_for_tenant};
27pub use create_setup::{get_cloud_user, handle_local_tenant_setup};
28
29use crate::cli_settings::CliConfig;
30use crate::shared::render_result;
31use anyhow::Result;
32use dialoguer::Select;
33use dialoguer::theme::ColorfulTheme;
34use systemprompt_cloud::{ProfilePath, ProjectContext};
35use systemprompt_logging::CliService;
36
37pub async fn execute(cmd: Option<ProfileCommands>, config: &CliConfig) -> Result<()> {
38    if let Some(cmd) = cmd {
39        execute_command(cmd, config).await.map(drop)
40    } else {
41        if !config.is_interactive() {
42            return Err(anyhow::anyhow!(
43                "Profile subcommand required in non-interactive mode"
44            ));
45        }
46        while let Some(cmd) = select_operation()? {
47            if execute_command(cmd, config).await? {
48                break;
49            }
50        }
51        Ok(())
52    }
53}
54
55async fn execute_command(cmd: ProfileCommands, config: &CliConfig) -> Result<bool> {
56    match cmd {
57        ProfileCommands::Create(args) => create::execute(&args, config).await.map(|()| true),
58        ProfileCommands::List => {
59            let result = list::execute(config)?;
60            render_result(&result);
61            Ok(false)
62        },
63        ProfileCommands::Show {
64            name,
65            filter,
66            json,
67            yaml,
68        } => show::execute(name.as_deref(), filter, json, yaml, config).map(|()| false),
69        ProfileCommands::Delete(args) => {
70            let result = delete::execute(&args, config)?;
71            render_result(&result);
72            Ok(false)
73        },
74        ProfileCommands::Edit(args) => edit::execute(&args, config).map(|()| false),
75    }
76}
77
78fn select_operation() -> Result<Option<ProfileCommands>> {
79    let ctx = ProjectContext::discover();
80    let profiles_dir = ctx.profiles_dir();
81    let has_profiles = profiles_dir.exists()
82        && std::fs::read_dir(&profiles_dir).is_ok_and(|entries| {
83            entries
84                .filter_map(Result::ok)
85                .any(|e| e.path().is_dir() && ProfilePath::Config.resolve(&e.path()).exists())
86        });
87
88    let edit_label = if has_profiles {
89        "Edit".to_owned()
90    } else {
91        "Edit (unavailable - no profiles)".to_owned()
92    };
93    let delete_label = if has_profiles {
94        "Delete".to_owned()
95    } else {
96        "Delete (unavailable - no profiles)".to_owned()
97    };
98
99    let operations = vec![
100        "List".to_owned(),
101        edit_label,
102        delete_label,
103        "Done".to_owned(),
104    ];
105
106    let selection = Select::with_theme(&ColorfulTheme::default())
107        .with_prompt("Profile operation")
108        .items(&operations)
109        .default(0)
110        .interact()?;
111
112    let cmd = match selection {
113        0 => Some(ProfileCommands::List),
114        1 | 2 if !has_profiles => {
115            CliService::warning("No profiles found");
116            CliService::info(
117                "Run 'systemprompt cloud tenant create' (or 'just tenant') to create a tenant \
118                 with a profile.",
119            );
120            return Ok(Some(ProfileCommands::List));
121        },
122        1 => Some(ProfileCommands::Edit(EditArgs {
123            name: None,
124            set_anthropic_key: None,
125            set_openai_key: None,
126            set_gemini_key: None,
127            set_github_token: None,
128            set_database_url: None,
129            set_external_url: None,
130            set_host: None,
131            set_port: None,
132        })),
133        2 => select_profile("Select profile to delete")?
134            .map(|name| ProfileCommands::Delete(DeleteArgs { name, yes: false })),
135        3 => None,
136        _ => unreachable!(),
137    };
138
139    Ok(cmd)
140}
141
142fn select_profile(prompt: &str) -> Result<Option<String>> {
143    let ctx = ProjectContext::discover();
144    let profiles_dir = ctx.profiles_dir();
145
146    if !profiles_dir.exists() {
147        CliService::warning("No profiles directory found.");
148        return Ok(None);
149    }
150
151    let profiles: Vec<String> = std::fs::read_dir(&profiles_dir)?
152        .filter_map(Result::ok)
153        .filter(|e| e.path().is_dir() && ProfilePath::Config.resolve(&e.path()).exists())
154        .filter_map(|e| e.file_name().to_str().map(String::from))
155        .collect();
156
157    if profiles.is_empty() {
158        CliService::warning("No profiles found.");
159        return Ok(None);
160    }
161
162    let selection = Select::with_theme(&ColorfulTheme::default())
163        .with_prompt(prompt)
164        .items(&profiles)
165        .default(0)
166        .interact()?;
167
168    Ok(Some(profiles[selection].clone()))
169}