systemprompt_cli/commands/cloud/profile/
mod.rs1mod api_keys;
2mod builders;
3mod create;
4mod create_setup;
5mod create_tenant;
6mod delete;
7mod edit;
8mod edit_secrets;
9mod edit_settings;
10mod list;
11mod show;
12mod show_display;
13mod show_types;
14mod templates;
15
16pub use api_keys::collect_api_keys;
17pub use create::create_profile_for_tenant;
18pub use create_setup::{get_cloud_user, handle_local_tenant_setup};
19
20use crate::cli_settings::CliConfig;
21use anyhow::Result;
22use clap::{Args, Subcommand, ValueEnum};
23use dialoguer::theme::ColorfulTheme;
24use dialoguer::{Input, Select};
25use systemprompt_cloud::{ProfilePath, ProjectContext};
26use systemprompt_logging::CliService;
27
28#[derive(Debug, Subcommand)]
29pub enum ProfileCommands {
30 #[command(about = "Create a new profile")]
31 Create(CreateArgs),
32
33 #[command(about = "List all profiles")]
34 List,
35
36 #[command(
37 about = "Show profile configuration",
38 after_help = "EXAMPLES:\n systemprompt cloud profile show\n systemprompt cloud profile \
39 show --filter agents\n systemprompt cloud profile show --json"
40 )]
41 Show {
42 name: Option<String>,
43
44 #[arg(short, long, value_enum, default_value = "all")]
45 filter: ShowFilter,
46
47 #[arg(long, help = "Output as JSON")]
48 json: bool,
49
50 #[arg(long, help = "Output as YAML")]
51 yaml: bool,
52 },
53
54 #[command(about = "Delete a profile")]
55 Delete(DeleteArgs),
56
57 #[command(about = "Edit profile configuration")]
58 Edit(EditArgs),
59}
60
61#[derive(Debug, Args)]
62pub struct DeleteArgs {
63 pub name: String,
64
65 #[arg(short = 'y', long, help = "Skip confirmation prompts")]
66 pub yes: bool,
67}
68
69#[derive(Debug, Args)]
70pub struct CreateArgs {
71 pub name: String,
72
73 #[arg(
74 long,
75 env = "SYSTEMPROMPT_TENANT_ID",
76 help = "Tenant ID (required in non-interactive mode)"
77 )]
78 pub tenant_id: Option<String>,
79
80 #[arg(long, value_enum, default_value = "local", help = "Tenant type")]
81 pub tenant_type: TenantTypeArg,
82
83 #[arg(long, env = "ANTHROPIC_API_KEY", help = "Anthropic (Claude) API key")]
84 pub anthropic_key: Option<String>,
85
86 #[arg(long, env = "OPENAI_API_KEY", help = "OpenAI (GPT) API key")]
87 pub openai_key: Option<String>,
88
89 #[arg(long, env = "GEMINI_API_KEY", help = "Google AI (Gemini) API key")]
90 pub gemini_key: Option<String>,
91
92 #[arg(long, env = "GITHUB_TOKEN", help = "GitHub token (optional)")]
93 pub github_token: Option<String>,
94}
95
96impl CreateArgs {
97 pub const fn has_api_key(&self) -> bool {
98 self.anthropic_key.is_some() || self.openai_key.is_some() || self.gemini_key.is_some()
99 }
100}
101
102#[derive(Clone, Copy, Debug, ValueEnum)]
103pub enum TenantTypeArg {
104 Local,
105 Cloud,
106}
107
108#[derive(Debug, Args)]
109pub struct EditArgs {
110 pub name: Option<String>,
111
112 #[arg(long, help = "Set Anthropic API key")]
113 pub set_anthropic_key: Option<String>,
114
115 #[arg(long, help = "Set OpenAI API key")]
116 pub set_openai_key: Option<String>,
117
118 #[arg(long, help = "Set Gemini API key")]
119 pub set_gemini_key: Option<String>,
120
121 #[arg(long, help = "Set GitHub token")]
122 pub set_github_token: Option<String>,
123
124 #[arg(long, help = "Set database URL")]
125 pub set_database_url: Option<String>,
126
127 #[arg(long, help = "Set external URL (cloud profiles)")]
128 pub set_external_url: Option<String>,
129
130 #[arg(long, help = "Set server host")]
131 pub set_host: Option<String>,
132
133 #[arg(long, help = "Set server port")]
134 pub set_port: Option<u16>,
135}
136
137impl EditArgs {
138 pub const fn has_updates(&self) -> bool {
139 self.set_anthropic_key.is_some()
140 || self.set_openai_key.is_some()
141 || self.set_gemini_key.is_some()
142 || self.set_github_token.is_some()
143 || self.set_database_url.is_some()
144 || self.set_external_url.is_some()
145 || self.set_host.is_some()
146 || self.set_port.is_some()
147 }
148}
149
150#[derive(Clone, Copy, Debug, ValueEnum)]
151pub enum ShowFilter {
152 All,
153 Agents,
154 Mcp,
155 Skills,
156 Ai,
157 Web,
158 Content,
159 Env,
160 Settings,
161}
162
163pub async fn execute(cmd: Option<ProfileCommands>, config: &CliConfig) -> Result<()> {
164 if let Some(cmd) = cmd {
165 execute_command(cmd, config).await.map(drop)
166 } else {
167 if !config.is_interactive() {
168 return Err(anyhow::anyhow!(
169 "Profile subcommand required in non-interactive mode"
170 ));
171 }
172 while let Some(cmd) = select_operation()? {
173 if execute_command(cmd, config).await? {
174 break;
175 }
176 }
177 Ok(())
178 }
179}
180
181async fn execute_command(cmd: ProfileCommands, config: &CliConfig) -> Result<bool> {
182 match cmd {
183 ProfileCommands::Create(args) => create::execute(&args, config).await.map(|()| true),
184 ProfileCommands::List => list::execute(config).map(|()| false),
185 ProfileCommands::Show {
186 name,
187 filter,
188 json,
189 yaml,
190 } => show::execute(name.as_deref(), filter, json, yaml, config).map(|()| false),
191 ProfileCommands::Delete(args) => delete::execute(&args, config).map(|()| false),
192 ProfileCommands::Edit(args) => edit::execute(&args, config).await.map(|()| false),
193 }
194}
195
196fn select_operation() -> Result<Option<ProfileCommands>> {
197 let ctx = ProjectContext::discover();
198 let profiles_dir = ctx.profiles_dir();
199 let has_profiles = profiles_dir.exists()
200 && std::fs::read_dir(&profiles_dir)
201 .map(|entries| {
202 entries
203 .filter_map(Result::ok)
204 .any(|e| e.path().is_dir() && ProfilePath::Config.resolve(&e.path()).exists())
205 })
206 .unwrap_or(false);
207
208 let edit_label = if has_profiles {
209 "Edit".to_string()
210 } else {
211 "Edit (unavailable - no profiles)".to_string()
212 };
213 let delete_label = if has_profiles {
214 "Delete".to_string()
215 } else {
216 "Delete (unavailable - no profiles)".to_string()
217 };
218
219 let operations = vec![
220 "Create".to_string(),
221 "List".to_string(),
222 edit_label,
223 delete_label,
224 "Done".to_string(),
225 ];
226
227 let selection = Select::with_theme(&ColorfulTheme::default())
228 .with_prompt("Profile operation")
229 .items(&operations)
230 .default(0)
231 .interact()?;
232
233 let cmd = match selection {
234 0 => {
235 let name: String = Input::with_theme(&ColorfulTheme::default())
236 .with_prompt("Profile name")
237 .interact_text()?;
238 Some(ProfileCommands::Create(CreateArgs {
239 name,
240 tenant_id: None,
241 tenant_type: TenantTypeArg::Local,
242 anthropic_key: None,
243 openai_key: None,
244 gemini_key: None,
245 github_token: None,
246 }))
247 },
248 1 => Some(ProfileCommands::List),
249 2 | 3 if !has_profiles => {
250 CliService::warning("No profiles found");
251 CliService::info("Run 'systemprompt cloud profile create <name>' to create one.");
252 return Ok(Some(ProfileCommands::List));
253 },
254 2 => Some(ProfileCommands::Edit(EditArgs {
255 name: None,
256 set_anthropic_key: None,
257 set_openai_key: None,
258 set_gemini_key: None,
259 set_github_token: None,
260 set_database_url: None,
261 set_external_url: None,
262 set_host: None,
263 set_port: None,
264 })),
265 3 => select_profile("Select profile to delete")?
266 .map(|name| ProfileCommands::Delete(DeleteArgs { name, yes: false })),
267 4 => None,
268 _ => unreachable!(),
269 };
270
271 Ok(cmd)
272}
273
274fn select_profile(prompt: &str) -> Result<Option<String>> {
275 let ctx = ProjectContext::discover();
276 let profiles_dir = ctx.profiles_dir();
277
278 if !profiles_dir.exists() {
279 CliService::warning("No profiles directory found.");
280 return Ok(None);
281 }
282
283 let profiles: Vec<String> = std::fs::read_dir(&profiles_dir)?
284 .filter_map(Result::ok)
285 .filter(|e| e.path().is_dir() && ProfilePath::Config.resolve(&e.path()).exists())
286 .filter_map(|e| e.file_name().to_str().map(String::from))
287 .collect();
288
289 if profiles.is_empty() {
290 CliService::warning("No profiles found.");
291 return Ok(None);
292 }
293
294 let selection = Select::with_theme(&ColorfulTheme::default())
295 .with_prompt(prompt)
296 .items(&profiles)
297 .default(0)
298 .interact()?;
299
300 Ok(Some(profiles[selection].clone()))
301}