Skip to main content

systemprompt_cli/commands/cloud/
mod.rs

1//! `cloud` command tree for systemprompt.io Cloud.
2//!
3//! Routes [`CloudCommands`] to the auth, init, tenant, profile, deploy,
4//! backup, status, restart, secrets, dockerfile, db, and domain subcommands,
5//! and declares each command's profile/secret requirements via
6//! [`DescribeCommand`].
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11pub mod auth;
12pub mod backup;
13pub mod db;
14pub mod deploy;
15pub mod doctor;
16pub mod domain;
17pub mod init;
18pub mod profile;
19mod restart;
20pub mod secrets;
21mod status;
22pub mod templates;
23pub mod tenant;
24pub mod types;
25
26pub use systemprompt_cloud::{Environment, OAuthProvider};
27
28use crate::cli_settings::CliConfig;
29use crate::context::CommandContext;
30use crate::descriptor::{CommandDescriptor, DescribeCommand};
31use anyhow::Result;
32use clap::Subcommand;
33
34#[derive(Debug, Subcommand)]
35pub enum CloudCommands {
36    #[command(
37        subcommand,
38        about = "Authentication (login, logout, whoami, admin-user)"
39    )]
40    Auth(auth::AuthCommands),
41
42    #[command(about = "Initialize project structure")]
43    Init {
44        #[arg(long)]
45        force: bool,
46    },
47
48    #[command(subcommand_required = false, about = "Manage tenants (local or cloud)")]
49    Tenant {
50        #[command(subcommand)]
51        command: Option<tenant::TenantCommands>,
52    },
53
54    #[command(subcommand_required = false, about = "Manage profiles")]
55    Profile {
56        #[command(subcommand)]
57        command: Option<profile::ProfileCommands>,
58    },
59
60    #[command(about = "Deploy to systemprompt.io Cloud")]
61    Deploy {
62        #[arg(long)]
63        skip_push: bool,
64
65        #[arg(long, short = 'p', help = "Profile name to deploy")]
66        profile: Option<String>,
67
68        #[arg(long, help = "Run the pre-deploy preflight only, without deploying")]
69        check: bool,
70    },
71
72    #[command(about = "Download the tenant's runtime services/ tree to a local directory")]
73    Backup {
74        #[arg(long, short = 'p', help = "Profile name to back up")]
75        profile: Option<String>,
76
77        #[arg(
78            long,
79            short = 'o',
80            help = "Output directory (default: ./systemprompt-backup-<timestamp>)"
81        )]
82        output: Option<std::path::PathBuf>,
83
84        #[arg(long, help = "List remote files without downloading")]
85        list: bool,
86    },
87
88    #[command(about = "Run the pre-deploy preflight for a profile without deploying")]
89    Doctor {
90        #[arg(long, short = 'p', help = "Profile name to check")]
91        profile: Option<String>,
92    },
93
94    #[command(about = "Check cloud deployment status")]
95    Status,
96
97    #[command(about = "Restart tenant machine")]
98    Restart {
99        #[arg(long)]
100        tenant: Option<String>,
101
102        #[arg(short = 'y', long, help = "Skip confirmation prompts")]
103        yes: bool,
104    },
105
106    #[command(subcommand, about = "Manage secrets for cloud tenant")]
107    Secrets(secrets::SecretsCommands),
108
109    #[command(about = "Generate Dockerfile based on discovered extensions")]
110    Dockerfile,
111
112    #[command(subcommand, about = "Cloud database operations")]
113    Db(db::CloudDbCommands),
114
115    #[command(subcommand, about = "Manage custom domain and TLS certificates")]
116    Domain(domain::DomainCommands),
117}
118
119impl DescribeCommand for CloudCommands {
120    fn descriptor(&self) -> CommandDescriptor {
121        match self {
122            Self::Deploy { .. } | Self::Secrets { .. } => CommandDescriptor::PROFILE_AND_SECRETS,
123            Self::Backup { .. } | Self::Status | Self::Restart { .. } | Self::Domain { .. } => {
124                CommandDescriptor::PROFILE_ONLY
125            },
126            _ => CommandDescriptor::NONE,
127        }
128    }
129}
130
131impl CloudCommands {
132    pub const fn requires_profile(&self) -> bool {
133        matches!(
134            self,
135            Self::Status | Self::Restart { .. } | Self::Secrets { .. } | Self::Domain { .. }
136        )
137    }
138
139    pub const fn requires_secrets(&self) -> bool {
140        matches!(self, Self::Secrets { .. })
141    }
142}
143
144pub async fn execute(cmd: CloudCommands, ctx: &CommandContext) -> Result<()> {
145    match cmd {
146        CloudCommands::Auth(cmd) => auth::execute(cmd, ctx).await,
147        CloudCommands::Init { force } => init::execute(force, &ctx.cli),
148        CloudCommands::Tenant { command } => tenant::execute(command, ctx).await,
149        CloudCommands::Profile { command } => profile::execute(command, ctx).await,
150        CloudCommands::Deploy {
151            skip_push,
152            profile,
153            check,
154        } => {
155            deploy::execute(
156                deploy::DeployArgs {
157                    skip_push,
158                    profile_name: profile,
159                    check,
160                },
161                ctx.prompter(),
162                &ctx.cli,
163            )
164            .await
165        },
166        CloudCommands::Backup {
167            profile,
168            output,
169            list,
170        } => {
171            backup::execute(
172                backup::BackupArgs {
173                    profile_name: profile,
174                    output,
175                    list,
176                },
177                ctx.prompter(),
178                &ctx.cli,
179            )
180            .await
181        },
182        CloudCommands::Doctor { profile } => {
183            doctor::execute(profile, ctx.prompter(), &ctx.cli).await
184        },
185        CloudCommands::Status => {
186            let result = status::execute(&ctx.cli).await?;
187            crate::shared::render_result(&result, &ctx.cli);
188            Ok(())
189        },
190        CloudCommands::Restart { tenant, yes } => {
191            let result = restart::execute(tenant, yes, ctx.prompter(), &ctx.cli).await?;
192            crate::shared::render_result(&result, &ctx.cli);
193            Ok(())
194        },
195        CloudCommands::Secrets(cmd) => secrets::execute(cmd, ctx).await,
196        CloudCommands::Dockerfile => execute_dockerfile(&ctx.cli),
197        CloudCommands::Db(cmd) => match ctx.database_url() {
198            Some(database_url) => db::execute_with_database_url(cmd, database_url, ctx).await,
199            None => db::execute(cmd, ctx).await,
200        },
201        CloudCommands::Domain(cmd) => domain::execute(cmd, ctx).await,
202    }
203}
204
205fn execute_dockerfile(config: &CliConfig) -> Result<()> {
206    use crate::shared::project::ProjectRoot;
207    use types::DockerfileOutput;
208
209    let project = ProjectRoot::discover().map_err(|e| anyhow::anyhow!("{}", e))?;
210    let content = systemprompt_cloud::deploy::generate_dockerfile_content(project.as_path());
211
212    let output = DockerfileOutput {
213        content: content.clone(),
214    };
215
216    if config.is_json_output() {
217        crate::shared::render_result(
218            &crate::shared::CommandOutput::copy_paste_titled("Dockerfile", output.content),
219            config,
220        );
221    } else {
222        systemprompt_logging::CliService::output(&content);
223    }
224
225    Ok(())
226}