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