systemprompt-cli 0.26.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
//! `cloud deploy` command: prompts, preflight, and progress rendering.
//!
//! Resolves the active cloud profile and tenant, runs the `cloud doctor`
//! preflight (deploys hard-block on a failing report), then hands the typed
//! request to [`DeployOrchestrator`] in the [`pipeline`] module, which owns
//! the image build/push, secret provisioning, and the deploy call. Rendering
//! flows back through [`CliDeployProgress`].
//!
//! Deploys are stateless container rebuilds: runtime files created inside the
//! previous container are not preserved. Use `systemprompt cloud backup`
//! first to keep a copy.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

pub mod pipeline;
pub mod progress;
mod select;

pub(in crate::commands::cloud) use progress::CliDeployProgress;
pub(in crate::commands::cloud) use select::resolve_profile;

use anyhow::{Context, Result, anyhow, bail};
use systemprompt_cloud::{CloudPath, ProfilePath, TenantStore, get_cloud_paths};
use systemprompt_identifiers::TenantId;
use systemprompt_logging::CliService;

use pipeline::{DeployOptions, DeployOrchestrator, DeployRequest};

use super::tenant::get_credentials;
use crate::cli_settings::CliConfig;
use crate::interactive::Prompter;
use crate::shared::project::ProjectRoot;

pub(super) struct DeployArgs {
    pub skip_push: bool,
    pub profile_name: Option<String>,
    pub check: bool,
}

pub(super) async fn execute(
    args: DeployArgs,
    prompter: &dyn Prompter,
    config: &CliConfig,
) -> Result<()> {
    CliService::section("systemprompt.io Cloud Deploy");

    let (profile, profile_path) = resolve_profile(prompter, args.profile_name.as_deref(), config)?;

    if profile.target != systemprompt_models::ProfileType::Cloud {
        bail!(
            "Cannot deploy a local profile. Create a cloud profile with: systemprompt cloud \
             profile create <name>"
        );
    }

    let profile_dir = profile_path
        .parent()
        .ok_or_else(|| anyhow!("Invalid profile path"))?;
    let report = super::doctor::run(&profile, profile_dir).await;
    report.render();
    if report.has_blocking() {
        bail!("Deploy preflight failed — fix the items above before deploying.");
    }
    if args.check {
        CliService::success("Deploy preflight passed (--check; nothing deployed)");
        return Ok(());
    }

    let target = resolve_deploy_target(&profile)?;
    let project = ProjectRoot::discover().map_err(|e| anyhow!("{}", e))?;

    let request = DeployRequest {
        tenant_id: target.tenant_id,
        tenant_name: target.tenant_name,
        profile_name: profile.name.clone(),
        project_root: project.as_path().to_path_buf(),
        credentials: target.creds,
        secrets_path: ProfilePath::Secrets.resolve(profile_dir),
        signing_key_path: super::doctor::resolve_signing_key_path(&profile, profile_dir),
        options: DeployOptions {
            skip_push: args.skip_push,
        },
    };

    let progress = CliDeployProgress::new();
    DeployOrchestrator::new()
        .deploy(&request, &progress)
        .await?;

    Ok(())
}

#[derive(Debug)]
pub struct DeployTarget {
    pub tenant_id: TenantId,
    pub tenant_name: String,
    pub hostname: Option<String>,
    pub creds: systemprompt_cloud::CloudCredentials,
}

pub fn resolve_deploy_target(profile: &systemprompt_models::Profile) -> Result<DeployTarget> {
    let cloud_config = profile
        .cloud
        .as_ref()
        .ok_or_else(|| anyhow!("No cloud configuration in profile"))?;

    let tenant_id = cloud_config
        .tenant_id
        .as_ref()
        .map(TenantId::new)
        .ok_or_else(|| anyhow!("No tenant configured. Run 'systemprompt cloud config'"))?;

    let creds = get_credentials()?;
    if creds.is_token_expired() {
        bail!("Token expired. Run 'systemprompt cloud login' to refresh.");
    }

    let cloud_paths = get_cloud_paths();
    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
    let tenant_store = TenantStore::load_from_path(&tenants_path)
        .context("Tenants not synced. Run 'systemprompt cloud login'")?;

    let tenant = tenant_store.find_tenant(&tenant_id).ok_or_else(|| {
        anyhow!(
            "Tenant {} not found. Run 'systemprompt cloud login'",
            tenant_id
        )
    })?;

    Ok(DeployTarget {
        tenant_id,
        tenant_name: tenant.name.clone(),
        hostname: tenant.hostname.clone(),
        creds,
    })
}