Skip to main content

systemprompt_cli/commands/cloud/deploy/
progress.rs

1//! CLI rendering for the deploy pipeline.
2//!
3//! [`CliDeployProgress`] implements the orchestrator's
4//! [`DeployProgress`] seam over `CliService` spinners and message sinks:
5//! sequencing lives in [`super::pipeline`], presentation lives here. A single
6//! spinner slot is cleared at every event boundary so each long-running step
7//! replaces the previous indicator.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use std::sync::Mutex;
13
14use indicatif::ProgressBar;
15use systemprompt_logging::CliService;
16
17use super::pipeline::{DeployEvent, DeployProgress};
18
19pub struct CliDeployProgress {
20    spinner: Mutex<Option<ProgressBar>>,
21}
22
23impl std::fmt::Debug for CliDeployProgress {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("CliDeployProgress").finish_non_exhaustive()
26    }
27}
28
29impl CliDeployProgress {
30    pub const fn new() -> Self {
31        Self {
32            spinner: Mutex::new(None),
33        }
34    }
35
36    fn start_spinner(&self, message: &str) {
37        if let Ok(mut slot) = self.spinner.lock() {
38            *slot = Some(CliService::spinner(message));
39        }
40    }
41
42    fn clear_spinner(&self) {
43        if let Ok(mut slot) = self.spinner.lock()
44            && let Some(spinner) = slot.take()
45        {
46            spinner.finish_and_clear();
47        }
48    }
49}
50
51impl Default for CliDeployProgress {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl DeployProgress for CliDeployProgress {
58    fn event(&self, event: &DeployEvent<'_>) {
59        self.clear_spinner();
60        if let Some(message) = spinner_message(event) {
61            self.start_spinner(message);
62            return;
63        }
64        render_event(event);
65    }
66}
67
68pub const fn spinner_message(event: &DeployEvent<'_>) -> Option<&'static str> {
69    match event {
70        DeployEvent::RegistryAuthStarted => Some("Fetching registry credentials..."),
71        DeployEvent::BuildStarted => Some("Building Docker image..."),
72        DeployEvent::PushStarted => Some("Pushing to registry..."),
73        DeployEvent::SecretsSyncStarted => Some("Syncing secrets..."),
74        DeployEvent::CredentialsSyncStarted => Some("Syncing cloud credentials..."),
75        DeployEvent::DeployStarted => Some("Deploying..."),
76        _ => None,
77    }
78}
79
80fn render_event(event: &DeployEvent<'_>) {
81    match event {
82        DeployEvent::ArtifactsResolved {
83            tenant_name,
84            binary,
85            dockerfile,
86        } => {
87            CliService::key_value("Tenant", tenant_name);
88            CliService::key_value("Binary", &binary.display().to_string());
89            CliService::key_value("Dockerfile", &dockerfile.display().to_string());
90        },
91        DeployEvent::ImageResolved { image } => CliService::key_value("Image", image),
92        DeployEvent::BuildFinished => CliService::success("Docker image built"),
93        DeployEvent::PushSkipped => CliService::info("Push skipped (--skip-push)"),
94        DeployEvent::PushFinished => CliService::success("Image pushed"),
95        DeployEvent::SecretsPhaseStarted => CliService::section("Provisioning Secrets"),
96        DeployEvent::SecretsFileMissing => {
97            CliService::warning("No secrets.json found - skipping secrets sync");
98        },
99        DeployEvent::SecretsSynced { count } => {
100            CliService::success(&format!("Synced {} secrets", count));
101        },
102        DeployEvent::CredentialsSynced { count } => {
103            CliService::success(&format!("Synced {} cloud credentials", count));
104        },
105        DeployEvent::ProfilePathConfigured => CliService::success("Profile path configured"),
106        DeployEvent::Deployed { status, app_url } => {
107            CliService::success("Deployed!");
108            CliService::key_value("Status", status);
109            if let Some(url) = app_url {
110                CliService::key_value("URL", url);
111            }
112        },
113        _ => {},
114    }
115}