Skip to main content

systemprompt_logging/services/cli/
service.rs

1//! The [`CliService`] facade over CLI output.
2//!
3//! Aggregates the display, prompt, table, and progress helpers into a single
4//! entry point for command code: levelled messages (which also publish a log
5//! event), structured output (`json`/`yaml`), spinners and progress bars, and
6//! module install/update prompts. Output is the sanctioned stderr/stdout sink,
7//! not `tracing`.
8
9use std::io::Write;
10use std::time::Duration;
11
12use crate::models::LoggingError;
13pub(super) type Result<T> = std::result::Result<T, LoggingError>;
14use indicatif::{ProgressBar, ProgressStyle};
15use serde::Serialize;
16use systemprompt_traits::LogEventLevel;
17
18use super::display::{CollectionDisplay, Display, DisplayUtils};
19use super::module::{BatchModuleOperations, ModuleDisplay, ModuleInstall, ModuleUpdate};
20use super::output::{mark_structured_emitted, publish_log};
21use super::prompts::{PromptBuilder, Prompts};
22use super::summary::{OperationResult, ProgressSummary, ValidationSummary};
23use super::theme::{EmphasisType, ItemStatus, MessageLevel, ModuleType, Theme};
24
25#[derive(Copy, Clone, Debug)]
26pub struct CliService;
27
28impl CliService {
29    pub fn success(message: &str) {
30        publish_log(LogEventLevel::Info, "cli", message);
31        DisplayUtils::message(MessageLevel::Success, message);
32    }
33
34    pub fn warning(message: &str) {
35        publish_log(LogEventLevel::Warn, "cli", message);
36        DisplayUtils::message(MessageLevel::Warning, message);
37    }
38
39    pub fn error(message: &str) {
40        publish_log(LogEventLevel::Error, "cli", message);
41        DisplayUtils::message(MessageLevel::Error, message);
42    }
43
44    pub fn info(message: &str) {
45        publish_log(LogEventLevel::Info, "cli", message);
46        DisplayUtils::message(MessageLevel::Info, message);
47    }
48
49    pub fn debug(message: &str) {
50        let debug_msg = format!("DEBUG: {message}");
51        publish_log(LogEventLevel::Debug, "cli", &debug_msg);
52        DisplayUtils::message(MessageLevel::Info, &debug_msg);
53    }
54
55    pub fn verbose(message: &str) {
56        publish_log(LogEventLevel::Debug, "cli", message);
57        DisplayUtils::message(MessageLevel::Info, message);
58    }
59
60    pub fn fatal(message: &str, exit_code: i32) -> ! {
61        let fatal_msg = format!("FATAL: {message}");
62        DisplayUtils::message(MessageLevel::Error, &fatal_msg);
63        std::process::exit(exit_code);
64    }
65
66    pub fn section(title: &str) {
67        DisplayUtils::section_header(title);
68    }
69
70    pub fn subsection(title: &str) {
71        DisplayUtils::subsection_header(title);
72    }
73
74    pub fn clear_screen() {
75        let mut stderr = std::io::stderr();
76        if let Err(e) = write!(stderr, "\x1B[2J\x1B[1;1H") {
77            tracing::debug!(error = %e, "cli clear_screen write failed");
78        }
79    }
80
81    pub fn output(content: &str) {
82        let mut stdout = std::io::stdout();
83        if let Err(e) = writeln!(stdout, "{content}") {
84            tracing::debug!(error = %e, "cli output write failed");
85        }
86    }
87
88    pub fn json<T: Serialize>(value: &T) {
89        mark_structured_emitted();
90        match serde_json::to_string_pretty(value) {
91            Ok(json) => {
92                let mut stdout = std::io::stdout();
93                if let Err(e) = writeln!(stdout, "{json}") {
94                    tracing::debug!(error = %e, "cli json write failed");
95                }
96            },
97            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
98        }
99    }
100
101    pub fn json_compact<T: Serialize>(value: &T) {
102        mark_structured_emitted();
103        match serde_json::to_string(value) {
104            Ok(json) => {
105                let mut stdout = std::io::stdout();
106                if let Err(e) = writeln!(stdout, "{json}") {
107                    tracing::debug!(error = %e, "cli json_compact write failed");
108                }
109            },
110            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
111        }
112    }
113
114    pub fn yaml<T: Serialize>(value: &T) {
115        mark_structured_emitted();
116        match serde_yaml::to_string(value) {
117            Ok(yaml) => {
118                let mut stdout = std::io::stdout();
119                if let Err(e) = write!(stdout, "{yaml}") {
120                    tracing::debug!(error = %e, "cli yaml write failed");
121                }
122            },
123            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
124        }
125    }
126
127    pub fn key_value(label: &str, value: &str) {
128        let mut stderr = std::io::stderr();
129        if let Err(e) = writeln!(
130            stderr,
131            "{}: {}",
132            Theme::color(label, EmphasisType::Bold),
133            Theme::color(value, EmphasisType::Highlight)
134        ) {
135            tracing::debug!(error = %e, "cli key_value write failed");
136        }
137    }
138
139    pub fn status_line(label: &str, value: &str, status: ItemStatus) {
140        let mut stderr = std::io::stderr();
141        if let Err(e) = writeln!(
142            stderr,
143            "{} {}: {}",
144            Theme::icon(status),
145            Theme::color(label, EmphasisType::Bold),
146            Theme::color(value, status)
147        ) {
148            tracing::debug!(error = %e, "cli status_line write failed");
149        }
150    }
151
152    pub fn spinner(message: &str) -> ProgressBar {
153        let pb = ProgressBar::new_spinner();
154        pb.set_style(
155            ProgressStyle::default_spinner()
156                .template("{spinner:.cyan} {msg}")
157                .unwrap_or_else(|_| ProgressStyle::default_spinner()),
158        );
159        pb.set_message(message.to_owned());
160        pb.enable_steady_tick(Duration::from_millis(100));
161        pb
162    }
163
164    pub fn progress_bar(total: u64) -> ProgressBar {
165        let pb = ProgressBar::new(total);
166        pb.set_style(
167            ProgressStyle::default_bar()
168                .template(
169                    "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
170                )
171                .unwrap_or_else(|_| ProgressStyle::default_bar())
172                .progress_chars("#>-"),
173        );
174        pb
175    }
176
177    pub fn timed<F, R>(label: &str, f: F) -> R
178    where
179        F: FnOnce() -> R,
180    {
181        let start = std::time::Instant::now();
182        let result = f();
183        let duration = start.elapsed();
184        let duration_secs = duration.as_secs_f64();
185        let info_msg = format!("{label} completed in {duration_secs:.2}s");
186        Self::info(&info_msg);
187        result
188    }
189
190    pub fn prompt_schemas(module_name: &str, schemas: &[(String, String)]) -> Result<bool> {
191        ModuleDisplay::prompt_apply_schemas(module_name, schemas)
192    }
193
194    pub fn prompt_seeds(module_name: &str, seeds: &[(String, String)]) -> Result<bool> {
195        ModuleDisplay::prompt_apply_seeds(module_name, seeds)
196    }
197
198    pub fn prompt_install(modules: &[String]) -> Result<bool> {
199        Prompts::confirm_install(modules)
200    }
201
202    pub fn prompt_update(updates: &[(String, String, String)]) -> Result<bool> {
203        Prompts::confirm_update(updates)
204    }
205
206    pub fn confirm(question: &str) -> Result<bool> {
207        Prompts::confirm(question, false)
208    }
209
210    pub fn confirm_default_yes(question: &str) -> Result<bool> {
211        Prompts::confirm(question, true)
212    }
213
214    pub fn display_validation_summary(summary: &ValidationSummary) {
215        summary.display();
216    }
217
218    pub fn display_result(result: &OperationResult) {
219        result.display();
220    }
221
222    pub fn display_progress(progress: &ProgressSummary) {
223        progress.display();
224    }
225
226    pub fn prompt_builder(message: &str) -> PromptBuilder {
227        PromptBuilder::new(message)
228    }
229
230    pub fn collection<T: Display>(title: &str, items: Vec<T>) -> CollectionDisplay<T> {
231        CollectionDisplay::new(title, items)
232    }
233
234    pub fn module_status(module_name: &str, message: &str) {
235        DisplayUtils::module_status(module_name, message);
236    }
237
238    pub fn relationship(from: &str, to: &str, status: ItemStatus, module_type: ModuleType) {
239        DisplayUtils::relationship(module_type, from, to, status);
240    }
241
242    pub fn item(status: ItemStatus, name: &str, detail: Option<&str>) {
243        DisplayUtils::item(status, name, detail);
244    }
245
246    pub fn batch_install(modules: &[ModuleInstall]) -> Result<bool> {
247        BatchModuleOperations::prompt_install_multiple(modules)
248    }
249
250    pub fn batch_update(updates: &[ModuleUpdate]) -> Result<bool> {
251        BatchModuleOperations::prompt_update_multiple(updates)
252    }
253
254    pub fn table(headers: &[&str], rows: &[Vec<String>]) {
255        super::table::render_table(headers, rows);
256    }
257}