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