systemprompt_cli/commands/cloud/tenant/
mod.rs1pub mod create;
11mod create_flow;
12pub(super) mod delete;
13pub mod docker;
14mod edit;
15mod list;
16mod rotate;
17pub mod select;
18mod show;
19
20pub use create::{create_external_tenant, create_local_tenant};
21pub use delete::delete_tenant;
22pub(in crate::commands::cloud) use docker::container::wait_for_postgres_healthy;
23pub use edit::edit_tenant;
24pub use list::list_tenants;
25pub use rotate::rotate_credentials;
26pub use select::{get_credentials, resolve_tenant_id, select_tenant};
27pub use show::show_tenant;
28
29use anyhow::Result;
30use clap::{Args, Subcommand};
31use systemprompt_cloud::{CloudPath, TenantStore, get_cloud_paths};
32use systemprompt_identifiers::TenantId;
33use systemprompt_logging::CliService;
34
35use crate::context::CommandContext;
36use crate::interactive::Prompter;
37use crate::shared::{parse_tenant_id, render_result};
38use create_flow::tenant_create;
39
40#[derive(Debug, Subcommand)]
41pub enum TenantCommands {
42 #[command(about = "Create a new tenant")]
43 Create,
44
45 #[command(
46 about = "List all tenants",
47 after_help = "EXAMPLES:\n systemprompt cloud tenant list\n systemprompt cloud tenant \
48 list --json"
49 )]
50 List,
51
52 #[command(about = "Show tenant details")]
53 Show {
54 #[arg(value_parser = parse_tenant_id)]
55 id: Option<TenantId>,
56 },
57
58 #[command(about = "Delete a tenant")]
59 Delete(TenantDeleteArgs),
60
61 #[command(about = "Edit tenant configuration")]
62 Edit {
63 #[arg(value_parser = parse_tenant_id)]
64 id: Option<TenantId>,
65 },
66
67 #[command(about = "Rotate database credentials")]
68 RotateCredentials(TenantRotateArgs),
69}
70
71#[derive(Debug, Args)]
72pub struct TenantRotateArgs {
73 #[arg(value_parser = parse_tenant_id)]
74 pub id: Option<TenantId>,
75
76 #[arg(short = 'y', long, help = "Skip confirmation prompts")]
77 pub yes: bool,
78}
79
80#[derive(Debug, Args)]
81pub struct TenantDeleteArgs {
82 #[arg(value_parser = parse_tenant_id)]
83 pub id: Option<TenantId>,
84
85 #[arg(short = 'y', long, help = "Skip confirmation prompts")]
86 pub yes: bool,
87}
88
89pub async fn execute(cmd: Option<TenantCommands>, ctx: &CommandContext) -> Result<()> {
90 if let Some(cmd) = cmd {
91 execute_command(cmd, ctx).await.map(drop)
92 } else {
93 if !ctx.cli.is_interactive() {
94 return Err(anyhow::anyhow!(
95 "Tenant subcommand required in non-interactive mode"
96 ));
97 }
98 while let Some(cmd) = select_operation(ctx.prompter())? {
99 if execute_command(cmd, ctx).await? {
100 break;
101 }
102 }
103 Ok(())
104 }
105}
106
107async fn execute_command(cmd: TenantCommands, ctx: &CommandContext) -> Result<bool> {
108 match cmd {
109 TenantCommands::Create => tenant_create(ctx.prompter(), &ctx.cli).await.map(|()| true),
110 TenantCommands::List => {
111 let result = list_tenants(ctx.prompter(), &ctx.cli).await?;
112 render_result(&result, &ctx.cli);
113 Ok(false)
114 },
115 TenantCommands::Show { id } => {
116 let result = show_tenant(ctx.prompter(), id.as_ref(), &ctx.cli)?;
117 render_result(&result, &ctx.cli);
118 Ok(false)
119 },
120 TenantCommands::Delete(args) => {
121 let result = delete_tenant(args, ctx.prompter(), &ctx.cli).await?;
122 render_result(&result, &ctx.cli);
123 Ok(false)
124 },
125 TenantCommands::Edit { id } => {
126 let result = edit_tenant(id, ctx.prompter(), &ctx.cli)?;
127 render_result(&result, &ctx.cli);
128 Ok(false)
129 },
130 TenantCommands::RotateCredentials(args) => {
131 let result = rotate_credentials(
132 args.id,
133 args.yes || !ctx.cli.is_interactive(),
134 ctx.prompter(),
135 &ctx.cli,
136 )
137 .await?;
138 render_result(&result, &ctx.cli);
139 Ok(false)
140 },
141 }
142}
143
144fn select_operation(prompter: &dyn Prompter) -> Result<Option<TenantCommands>> {
145 let cloud_paths = get_cloud_paths();
146 let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
147 let store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
148 CliService::warning(&format!("Failed to load tenant store: {}", e));
149 TenantStore::default()
150 });
151 let has_tenants = !store.tenants.is_empty();
152
153 choose_tenant_operation(prompter, has_tenants)
154}
155
156pub fn choose_tenant_operation(
157 prompter: &dyn Prompter,
158 has_tenants: bool,
159) -> Result<Option<TenantCommands>> {
160 let edit_label = if has_tenants {
161 "Edit".to_owned()
162 } else {
163 "Edit (unavailable - no tenants configured)".to_owned()
164 };
165 let delete_label = if has_tenants {
166 "Delete".to_owned()
167 } else {
168 "Delete (unavailable - no tenants configured)".to_owned()
169 };
170
171 let operations = vec![
172 "Create".to_owned(),
173 "List".to_owned(),
174 edit_label,
175 delete_label,
176 "Done".to_owned(),
177 ];
178
179 let selection = prompter.select("Tenant operation", &operations)?;
180
181 let cmd = match selection {
182 0 => Some(TenantCommands::Create),
183 1 => Some(TenantCommands::List),
184 2 | 3 if !has_tenants => {
185 CliService::warning("No tenants configured");
186 CliService::info("Run 'systemprompt cloud tenant create' to create one.");
187 return Ok(Some(TenantCommands::List));
188 },
189 2 => Some(TenantCommands::Edit { id: None }),
190 3 => Some(TenantCommands::Delete(TenantDeleteArgs {
191 id: None,
192 yes: false,
193 })),
194 4 => None,
195 other => return Err(anyhow::anyhow!("unexpected menu selection: {other}")),
196 };
197
198 Ok(cmd)
199}