systemprompt-cli 0.25.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
//! CLI rendering for the deploy pipeline.
//!
//! [`CliDeployProgress`] implements the orchestrator's
//! [`DeployProgress`] seam over `CliService` spinners and message sinks:
//! sequencing lives in [`super::pipeline`], presentation lives here. A single
//! spinner slot is cleared at every event boundary so each long-running step
//! replaces the previous indicator.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use std::sync::Mutex;

use indicatif::ProgressBar;
use systemprompt_logging::CliService;

use super::pipeline::{DeployEvent, DeployProgress};

pub struct CliDeployProgress {
    spinner: Mutex<Option<ProgressBar>>,
}

impl std::fmt::Debug for CliDeployProgress {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CliDeployProgress").finish_non_exhaustive()
    }
}

impl CliDeployProgress {
    pub const fn new() -> Self {
        Self {
            spinner: Mutex::new(None),
        }
    }

    fn start_spinner(&self, message: &str) {
        if let Ok(mut slot) = self.spinner.lock() {
            *slot = Some(CliService::spinner(message));
        }
    }

    fn clear_spinner(&self) {
        if let Ok(mut slot) = self.spinner.lock()
            && let Some(spinner) = slot.take()
        {
            spinner.finish_and_clear();
        }
    }
}

impl Default for CliDeployProgress {
    fn default() -> Self {
        Self::new()
    }
}

impl DeployProgress for CliDeployProgress {
    fn event(&self, event: &DeployEvent<'_>) {
        self.clear_spinner();
        if let Some(message) = spinner_message(event) {
            self.start_spinner(message);
            return;
        }
        render_event(event);
    }
}

pub const fn spinner_message(event: &DeployEvent<'_>) -> Option<&'static str> {
    match event {
        DeployEvent::RegistryAuthStarted => Some("Fetching registry credentials..."),
        DeployEvent::BuildStarted => Some("Building Docker image..."),
        DeployEvent::PushStarted => Some("Pushing to registry..."),
        DeployEvent::SecretsSyncStarted => Some("Syncing secrets..."),
        DeployEvent::CredentialsSyncStarted => Some("Syncing cloud credentials..."),
        DeployEvent::DeployStarted => Some("Deploying..."),
        _ => None,
    }
}

fn render_event(event: &DeployEvent<'_>) {
    match event {
        DeployEvent::ArtifactsResolved {
            tenant_name,
            binary,
            dockerfile,
        } => {
            CliService::key_value("Tenant", tenant_name);
            CliService::key_value("Binary", &binary.display().to_string());
            CliService::key_value("Dockerfile", &dockerfile.display().to_string());
        },
        DeployEvent::ImageResolved { image } => CliService::key_value("Image", image),
        DeployEvent::BuildFinished => CliService::success("Docker image built"),
        DeployEvent::PushSkipped => CliService::info("Push skipped (--skip-push)"),
        DeployEvent::PushFinished => CliService::success("Image pushed"),
        DeployEvent::SecretsPhaseStarted => CliService::section("Provisioning Secrets"),
        DeployEvent::SecretsFileMissing => {
            CliService::warning("No secrets.json found - skipping secrets sync");
        },
        DeployEvent::SecretsSynced { count } => {
            CliService::success(&format!("Synced {} secrets", count));
        },
        DeployEvent::CredentialsSynced { count } => {
            CliService::success(&format!("Synced {} cloud credentials", count));
        },
        DeployEvent::ProfilePathConfigured => CliService::success("Profile path configured"),
        DeployEvent::Deployed { status, app_url } => {
            CliService::success("Deployed!");
            CliService::key_value("Status", status);
            if let Some(url) = app_url {
                CliService::key_value("URL", url);
            }
        },
        _ => {},
    }
}