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