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