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//! status, restart, sync, secrets, dockerfile, db, and domain subcommands, and
5//! declares each command's profile/secret requirements via [`DescribeCommand`].
6
7pub mod auth;
8pub mod db;
9mod deploy;
10pub mod dockerfile;
11mod domain;
12mod init;
13pub mod profile;
14mod restart;
15mod secrets;
16mod status;
17pub mod sync;
18pub mod templates;
19pub mod tenant;
20pub mod types;
21
22pub use systemprompt_cloud::{Environment, OAuthProvider};
23
24use crate::cli_settings::CliConfig;
25use crate::descriptor::{CommandDescriptor, DescribeCommand};
26use anyhow::Result;
27use clap::Subcommand;
28
29#[derive(Debug, Subcommand)]
30pub enum CloudCommands {
31    #[command(subcommand, about = "Authentication (login, logout, whoami)")]
32    Auth(auth::AuthCommands),
33
34    #[command(about = "Initialize project structure")]
35    Init {
36        #[arg(long)]
37        force: bool,
38    },
39
40    #[command(subcommand_required = false, about = "Manage tenants (local or cloud)")]
41    Tenant {
42        #[command(subcommand)]
43        command: Option<tenant::TenantCommands>,
44    },
45
46    #[command(subcommand_required = false, about = "Manage profiles")]
47    Profile {
48        #[command(subcommand)]
49        command: Option<profile::ProfileCommands>,
50    },
51
52    #[command(about = "Deploy to systemprompt.io Cloud")]
53    Deploy {
54        #[arg(long)]
55        skip_push: bool,
56
57        #[arg(long, short = 'p', help = "Profile name to deploy")]
58        profile: Option<String>,
59
60        #[arg(
61            long,
62            help = "Skip pre-deploy sync from cloud (WARNING: may lose runtime files)"
63        )]
64        no_sync: bool,
65
66        #[arg(short = 'y', long, help = "Skip confirmation prompts")]
67        yes: bool,
68
69        #[arg(long, help = "Preview sync without deploying")]
70        dry_run: bool,
71    },
72
73    #[command(about = "Check cloud deployment status")]
74    Status,
75
76    #[command(about = "Restart tenant machine")]
77    Restart {
78        #[arg(long)]
79        tenant: Option<String>,
80
81        #[arg(short = 'y', long, help = "Skip confirmation prompts")]
82        yes: bool,
83    },
84
85    #[command(
86        subcommand_required = false,
87        about = "Sync between local and cloud environments"
88    )]
89    Sync {
90        #[command(subcommand)]
91        command: Option<sync::SyncCommands>,
92    },
93
94    #[command(subcommand, about = "Manage secrets for cloud tenant")]
95    Secrets(secrets::SecretsCommands),
96
97    #[command(about = "Generate Dockerfile based on discovered extensions")]
98    Dockerfile,
99
100    #[command(subcommand, about = "Cloud database operations")]
101    Db(db::CloudDbCommands),
102
103    #[command(subcommand, about = "Manage custom domain and TLS certificates")]
104    Domain(domain::DomainCommands),
105}
106
107impl DescribeCommand for CloudCommands {
108    fn descriptor(&self) -> CommandDescriptor {
109        match self {
110            Self::Deploy { .. } => CommandDescriptor::PROFILE_AND_SECRETS,
111            Self::Sync { command: Some(_) } | Self::Secrets { .. } => {
112                CommandDescriptor::PROFILE_AND_SECRETS
113            },
114            Self::Status | Self::Restart { .. } | Self::Domain { .. } => {
115                CommandDescriptor::PROFILE_ONLY
116            },
117            _ => CommandDescriptor::NONE,
118        }
119    }
120}
121
122impl CloudCommands {
123    pub const fn requires_profile(&self) -> bool {
124        matches!(
125            self,
126            Self::Sync { command: Some(_) }
127                | Self::Status
128                | Self::Restart { .. }
129                | Self::Secrets { .. }
130                | Self::Domain { .. }
131        )
132    }
133
134    pub const fn requires_secrets(&self) -> bool {
135        matches!(self, Self::Sync { command: Some(_) } | Self::Secrets { .. })
136    }
137}
138
139pub async fn execute(cmd: CloudCommands, config: &CliConfig) -> Result<()> {
140    match cmd {
141        CloudCommands::Auth(cmd) => auth::execute(cmd, config).await,
142        CloudCommands::Init { force } => init::execute(force, config),
143        CloudCommands::Tenant { command } => tenant::execute(command, config).await,
144        CloudCommands::Profile { command } => profile::execute(command, config).await,
145        CloudCommands::Deploy {
146            skip_push,
147            profile,
148            no_sync,
149            yes,
150            dry_run,
151        } => {
152            deploy::execute(
153                deploy::DeployArgs {
154                    skip_push,
155                    profile_name: profile,
156                    no_sync,
157                    yes,
158                    dry_run,
159                },
160                config,
161            )
162            .await
163        },
164        CloudCommands::Status => {
165            let result = status::execute(config).await?;
166            crate::shared::render_result(&result);
167            Ok(())
168        },
169        CloudCommands::Restart { tenant, yes } => {
170            let result = restart::execute(tenant, yes, config).await?;
171            crate::shared::render_result(&result);
172            Ok(())
173        },
174        CloudCommands::Sync { command } => sync::execute(command, config).await,
175        CloudCommands::Secrets(cmd) => secrets::execute(cmd, config).await,
176        CloudCommands::Dockerfile => execute_dockerfile(config),
177        CloudCommands::Db(cmd) => db::execute(cmd, config).await,
178        CloudCommands::Domain(cmd) => domain::execute(cmd, config).await,
179    }
180}
181
182fn execute_dockerfile(config: &CliConfig) -> Result<()> {
183    use crate::shared::project::ProjectRoot;
184    use types::DockerfileOutput;
185
186    let project = ProjectRoot::discover().map_err(|e| anyhow::anyhow!("{}", e))?;
187    let content = dockerfile::generate_dockerfile_content(project.as_path());
188
189    let output = DockerfileOutput {
190        content: content.clone(),
191    };
192
193    if config.is_json_output() {
194        crate::shared::render_result(
195            &crate::shared::CommandResult::copy_paste(output).with_title("Dockerfile"),
196        );
197    } else {
198        systemprompt_logging::CliService::output(&content);
199    }
200
201    Ok(())
202}