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