systemprompt_cli/commands/cloud/profile/
mod.rs1mod 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) => create::execute(&args, ctx.prompter(), &ctx.cli)
57 .await
58 .map(|()| true),
59 ProfileCommands::List => {
60 let result = list::execute(ctx)?;
61 render_result(&result, &ctx.cli);
62 Ok(false)
63 },
64 ProfileCommands::Show {
65 name,
66 filter,
67 json,
68 yaml,
69 } => show::execute(name.as_deref(), filter, json, yaml, ctx).map(|()| false),
70 ProfileCommands::Delete(args) => {
71 let result = delete::execute(&args, ctx.prompter(), &ctx.cli)?;
72 render_result(&result, &ctx.cli);
73 Ok(false)
74 },
75 ProfileCommands::Edit(args) => edit::execute(&args, ctx).map(|()| false),
76 }
77}
78
79fn select_operation(prompter: &dyn Prompter) -> Result<Option<ProfileCommands>> {
80 let ctx = ProjectContext::discover();
81 let profiles_dir = ctx.profiles_dir();
82 let has_profiles = profiles_dir.exists()
83 && std::fs::read_dir(&profiles_dir).is_ok_and(|entries| {
84 entries
85 .filter_map(Result::ok)
86 .any(|e| e.path().is_dir() && ProfilePath::Config.resolve(&e.path()).exists())
87 });
88
89 let edit_label = if has_profiles {
90 "Edit".to_owned()
91 } else {
92 "Edit (unavailable - no profiles)".to_owned()
93 };
94 let delete_label = if has_profiles {
95 "Delete".to_owned()
96 } else {
97 "Delete (unavailable - no profiles)".to_owned()
98 };
99
100 let operations = vec![
101 "List".to_owned(),
102 edit_label,
103 delete_label,
104 "Done".to_owned(),
105 ];
106
107 let selection = prompter.select("Profile operation", &operations)?;
108
109 let cmd = match selection {
110 0 => Some(ProfileCommands::List),
111 1 | 2 if !has_profiles => {
112 CliService::warning("No profiles found");
113 CliService::info(
114 "Run 'systemprompt cloud tenant create' (or 'just tenant') to create a tenant \
115 with a profile.",
116 );
117 return Ok(Some(ProfileCommands::List));
118 },
119 1 => Some(ProfileCommands::Edit(EditArgs {
120 name: None,
121 set_anthropic_key: None,
122 set_openai_key: None,
123 set_gemini_key: None,
124 set_github_token: None,
125 set_database_url: None,
126 set_external_url: None,
127 set_host: None,
128 set_port: None,
129 })),
130 2 => select_profile(prompter, "Select profile to delete")?
131 .map(|name| ProfileCommands::Delete(DeleteArgs { name, yes: false })),
132 3 => None,
133 other => return Err(anyhow::anyhow!("unexpected menu selection: {other}")),
134 };
135
136 Ok(cmd)
137}
138
139fn select_profile(prompter: &dyn Prompter, prompt: &str) -> Result<Option<String>> {
140 let ctx = ProjectContext::discover();
141 let profiles_dir = ctx.profiles_dir();
142
143 if !profiles_dir.exists() {
144 CliService::warning("No profiles directory found.");
145 return Ok(None);
146 }
147
148 let profiles: Vec<String> = std::fs::read_dir(&profiles_dir)?
149 .filter_map(Result::ok)
150 .filter(|e| e.path().is_dir() && ProfilePath::Config.resolve(&e.path()).exists())
151 .filter_map(|e| e.file_name().to_str().map(String::from))
152 .collect();
153
154 if profiles.is_empty() {
155 CliService::warning("No profiles found.");
156 return Ok(None);
157 }
158
159 let selection = prompter.select(prompt, &profiles)?;
160
161 Ok(Some(profiles[selection].clone()))
162}