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