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