systemprompt_cli/commands/cloud/tenant/
mod.rs1mod cancel;
8mod create;
9mod create_flow;
10pub(super) mod delete;
11mod docker;
12mod edit;
13mod list;
14mod rotate;
15mod select;
16mod show;
17mod validation;
18
19pub use cancel::cancel_subscription;
20pub use create::{
21 create_cloud_tenant, create_external_tenant, create_local_tenant, swap_to_external_host,
22};
23pub use delete::delete_tenant;
24pub(in crate::commands::cloud) use docker::wait_for_postgres_healthy;
25pub use edit::edit_tenant;
26pub use list::list_tenants;
27pub use rotate::rotate_credentials;
28pub use select::{get_credentials, resolve_tenant_id};
29pub use show::show_tenant;
30pub use validation::check_build_ready;
31
32use anyhow::Result;
33use clap::{Args, Subcommand};
34use dialoguer::Select;
35use dialoguer::theme::ColorfulTheme;
36use systemprompt_cloud::{CloudPath, TenantStore, get_cloud_paths};
37use systemprompt_logging::CliService;
38
39use crate::context::CommandContext;
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()? {
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.cli).await.map(|()| true),
116 TenantCommands::List => {
117 let result = list_tenants(&ctx.cli).await?;
118 render_result(&result, &ctx.cli);
119 Ok(false)
120 },
121 TenantCommands::Show { id } => {
122 let result = show_tenant(id.as_ref(), &ctx.cli)?;
123 render_result(&result, &ctx.cli);
124 Ok(false)
125 },
126 TenantCommands::Delete(args) => {
127 let result = delete_tenant(args, &ctx.cli).await?;
128 render_result(&result, &ctx.cli);
129 Ok(false)
130 },
131 TenantCommands::Edit { id } => {
132 let result = edit_tenant(id, &ctx.cli)?;
133 render_result(&result, &ctx.cli);
134 Ok(false)
135 },
136 TenantCommands::RotateCredentials(args) => {
137 let result =
138 rotate_credentials(args.id, args.yes || !ctx.cli.is_interactive(), &ctx.cli)
139 .await?;
140 render_result(&result, &ctx.cli);
141 Ok(false)
142 },
143 TenantCommands::Cancel(args) => {
144 let result = cancel_subscription(args, &ctx.cli).await?;
145 render_result(&result, &ctx.cli);
146 Ok(false)
147 },
148 }
149}
150
151fn select_operation() -> Result<Option<TenantCommands>> {
152 let cloud_paths = get_cloud_paths();
153 let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
154 let store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
155 CliService::warning(&format!("Failed to load tenant store: {}", e));
156 TenantStore::default()
157 });
158 let has_tenants = !store.tenants.is_empty();
159
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 = Select::with_theme(&ColorfulTheme::default())
180 .with_prompt("Tenant operation")
181 .items(&operations)
182 .default(0)
183 .interact()?;
184
185 let cmd = match selection {
186 0 => Some(TenantCommands::Create {
187 region: "iad".to_owned(),
188 }),
189 1 => Some(TenantCommands::List),
190 2 | 3 if !has_tenants => {
191 CliService::warning("No tenants configured");
192 CliService::info(
193 "Run 'systemprompt cloud tenant create' (or 'just tenant') to create one.",
194 );
195 return Ok(Some(TenantCommands::List));
196 },
197 2 => Some(TenantCommands::Edit { id: None }),
198 3 => Some(TenantCommands::Delete(TenantDeleteArgs {
199 id: None,
200 yes: false,
201 })),
202 4 => None,
203 _ => unreachable!(),
204 };
205
206 Ok(cmd)
207}