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