systemprompt_cli/commands/cloud/deploy/
mod.rs1pub mod pipeline;
17pub mod progress;
18pub mod select;
19
20pub(in crate::commands::cloud) use progress::CliDeployProgress;
21pub(in crate::commands::cloud) use select::resolve_profile;
22
23use anyhow::{Context, Result, anyhow, bail};
24use systemprompt_cloud::{CloudPath, ProfilePath, TenantStore, get_cloud_paths};
25use systemprompt_identifiers::TenantId;
26use systemprompt_logging::CliService;
27
28use pipeline::{DeployOptions, DeployOrchestrator, DeployRequest, DeploySecretsSource};
29
30use super::tenant::get_credentials;
31use crate::cli_settings::CliConfig;
32use crate::interactive::Prompter;
33use crate::shared::project::ProjectRoot;
34
35pub(super) struct DeployArgs {
36 pub skip_push: bool,
37 pub profile_name: Option<String>,
38 pub check: bool,
39}
40
41pub(super) async fn execute(
42 args: DeployArgs,
43 prompter: &dyn Prompter,
44 config: &CliConfig,
45) -> Result<()> {
46 CliService::section("systemprompt.io Cloud Deploy");
47
48 let (profile, profile_path) = resolve_profile(prompter, args.profile_name.as_deref(), config)?;
49
50 if profile.target != systemprompt_models::ProfileType::Cloud {
51 bail!(
52 "Cannot deploy a local profile. Create a cloud profile with: systemprompt cloud \
53 profile create <name>"
54 );
55 }
56
57 let profile_dir = profile_path
58 .parent()
59 .ok_or_else(|| anyhow!("Invalid profile path"))?;
60 let report = super::doctor::run(&profile, profile_dir, false).await;
61 report.render();
62 if report.has_blocking() {
63 bail!("Deploy preflight failed — fix the items above before deploying.");
64 }
65 if args.check {
66 CliService::success("Deploy preflight passed (--check; nothing deployed)");
67 return Ok(());
68 }
69
70 let target = resolve_deploy_target(&profile)?;
71 let project = ProjectRoot::discover().map_err(|e| anyhow!("{}", e))?;
72
73 let request = DeployRequest {
74 tenant_id: target.tenant_id,
75 tenant_name: target.tenant_name,
76 profile_name: profile.name.clone(),
77 project_root: project.as_path().to_path_buf(),
78 credentials: target.creds,
79 secrets: DeploySecretsSource::from_profile(
80 profile.secrets.as_ref(),
81 ProfilePath::Secrets.resolve(profile_dir),
82 ),
83 signing_key_path: super::doctor::resolve_signing_key_path(&profile, profile_dir),
84 options: DeployOptions {
85 skip_push: args.skip_push,
86 },
87 };
88
89 let progress = CliDeployProgress::new();
90 DeployOrchestrator::new()
91 .deploy(&request, &progress)
92 .await?;
93
94 Ok(())
95}
96
97#[derive(Debug)]
98pub struct DeployTarget {
99 pub tenant_id: TenantId,
100 pub tenant_name: String,
101 pub hostname: Option<String>,
102 pub creds: systemprompt_cloud::CloudCredentials,
103}
104
105pub fn resolve_deploy_target(profile: &systemprompt_models::Profile) -> Result<DeployTarget> {
106 let cloud_config = profile
107 .cloud
108 .as_ref()
109 .ok_or_else(|| anyhow!("No cloud configuration in profile"))?;
110
111 let tenant_id = cloud_config
112 .tenant_id
113 .as_ref()
114 .map(TenantId::new)
115 .ok_or_else(|| anyhow!("No tenant configured. Run 'systemprompt cloud config'"))?;
116
117 let creds = get_credentials()?;
118 if creds.is_token_expired() {
119 bail!("Token expired. Run 'systemprompt cloud auth login' to refresh.");
120 }
121
122 let cloud_paths = get_cloud_paths();
123 let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
124 let tenant_store = TenantStore::load_from_path(&tenants_path)
125 .context("Tenants not synced. Run 'systemprompt cloud auth login'")?;
126
127 let tenant = tenant_store.find_tenant(&tenant_id).ok_or_else(|| {
128 anyhow!(
129 "Tenant {} not found. Run 'systemprompt cloud auth login'",
130 tenant_id
131 )
132 })?;
133
134 Ok(DeployTarget {
135 tenant_id,
136 tenant_name: tenant.name.clone(),
137 hostname: tenant.hostname.clone(),
138 creds,
139 })
140}