systemprompt_cli/commands/cloud/profile/
mod.rs1mod 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}