systemprompt_cli/commands/cloud/
mod.rs1pub mod auth;
8pub mod db;
9mod deploy;
10pub mod doctor;
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::context::CommandContext;
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, ctx: &CommandContext) -> Result<()> {
150 match cmd {
151 CloudCommands::Auth(cmd) => auth::execute(cmd, ctx).await,
152 CloudCommands::Init { force } => init::execute(force, &ctx.cli),
153 CloudCommands::Tenant { command } => tenant::execute(command, ctx).await,
154 CloudCommands::Profile { command } => profile::execute(command, ctx).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 &ctx.cli,
173 )
174 .await
175 },
176 CloudCommands::Doctor { profile } => doctor::execute(profile, &ctx.cli).await,
177 CloudCommands::Status => {
178 let result = status::execute(&ctx.cli).await?;
179 crate::shared::render_result(&result, &ctx.cli);
180 Ok(())
181 },
182 CloudCommands::Restart { tenant, yes } => {
183 let result = restart::execute(tenant, yes, &ctx.cli).await?;
184 crate::shared::render_result(&result, &ctx.cli);
185 Ok(())
186 },
187 CloudCommands::Sync { command } => sync::execute(command, ctx).await,
188 CloudCommands::Secrets(cmd) => secrets::execute(cmd, ctx).await,
189 CloudCommands::Dockerfile => execute_dockerfile(&ctx.cli),
190 CloudCommands::Db(cmd) => match ctx.database_url() {
191 Some(database_url) => db::execute_with_database_url(cmd, database_url, ctx).await,
192 None => db::execute(cmd, ctx).await,
193 },
194 CloudCommands::Domain(cmd) => domain::execute(cmd, ctx).await,
195 }
196}
197
198fn execute_dockerfile(config: &CliConfig) -> Result<()> {
199 use crate::shared::project::ProjectRoot;
200 use types::DockerfileOutput;
201
202 let project = ProjectRoot::discover().map_err(|e| anyhow::anyhow!("{}", e))?;
203 let content = systemprompt_cloud::deploy::generate_dockerfile_content(project.as_path());
204
205 let output = DockerfileOutput {
206 content: content.clone(),
207 };
208
209 if config.is_json_output() {
210 crate::shared::render_result(
211 &crate::shared::CommandOutput::copy_paste_titled("Dockerfile", output.content),
212 config,
213 );
214 } else {
215 systemprompt_logging::CliService::output(&content);
216 }
217
218 Ok(())
219}