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