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