systemprompt_cli/commands/cloud/
mod.rs1pub 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}