Skip to main content

systemprompt_logging/services/cli/
service.rs

1#![allow(clippy::print_stdout)]
2
3use std::time::Duration;
4
5use anyhow::Result;
6use indicatif::{ProgressBar, ProgressStyle};
7use serde::Serialize;
8use systemprompt_traits::LogEventLevel;
9
10use super::display::{CollectionDisplay, Display, DisplayUtils};
11use super::module::{BatchModuleOperations, ModuleDisplay, ModuleInstall, ModuleUpdate};
12use super::output::publish_log;
13use super::prompts::{PromptBuilder, Prompts};
14use super::startup::{
15    render_phase_header, render_phase_info, render_phase_success, render_phase_warning,
16    render_startup_banner,
17};
18use super::summary::{OperationResult, ProgressSummary, ValidationSummary};
19use super::table::{render_service_table, render_startup_complete, ServiceTableEntry};
20use super::theme::{EmphasisType, ItemStatus, MessageLevel, ModuleType, Theme};
21
22#[derive(Copy, Clone, Debug)]
23pub struct CliService;
24
25impl CliService {
26    pub fn success(message: &str) {
27        publish_log(LogEventLevel::Info, "cli", message);
28        DisplayUtils::message(MessageLevel::Success, message);
29    }
30
31    pub fn warning(message: &str) {
32        publish_log(LogEventLevel::Warn, "cli", message);
33        DisplayUtils::message(MessageLevel::Warning, message);
34    }
35
36    pub fn error(message: &str) {
37        publish_log(LogEventLevel::Error, "cli", message);
38        DisplayUtils::message(MessageLevel::Error, message);
39    }
40
41    pub fn info(message: &str) {
42        publish_log(LogEventLevel::Info, "cli", message);
43        DisplayUtils::message(MessageLevel::Info, message);
44    }
45
46    pub fn debug(message: &str) {
47        let debug_msg = format!("DEBUG: {message}");
48        publish_log(LogEventLevel::Debug, "cli", &debug_msg);
49        DisplayUtils::message(MessageLevel::Info, &debug_msg);
50    }
51
52    pub fn verbose(message: &str) {
53        publish_log(LogEventLevel::Debug, "cli", message);
54        DisplayUtils::message(MessageLevel::Info, message);
55    }
56
57    #[allow(clippy::exit)]
58    pub fn fatal(message: &str, exit_code: i32) -> ! {
59        let fatal_msg = format!("FATAL: {message}");
60        DisplayUtils::message(MessageLevel::Error, &fatal_msg);
61        std::process::exit(exit_code);
62    }
63
64    pub fn section(title: &str) {
65        DisplayUtils::section_header(title);
66    }
67
68    pub fn subsection(title: &str) {
69        DisplayUtils::subsection_header(title);
70    }
71
72    pub fn clear_screen() {
73        print!("\x1B[2J\x1B[1;1H");
74    }
75
76    pub fn output(content: &str) {
77        println!("{content}");
78    }
79
80    pub fn json<T: Serialize>(value: &T) {
81        match serde_json::to_string_pretty(value) {
82            Ok(json) => println!("{json}"),
83            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
84        }
85    }
86
87    pub fn json_compact<T: Serialize>(value: &T) {
88        match serde_json::to_string(value) {
89            Ok(json) => println!("{json}"),
90            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
91        }
92    }
93
94    pub fn yaml<T: Serialize>(value: &T) {
95        match serde_yaml::to_string(value) {
96            Ok(yaml) => print!("{yaml}"),
97            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
98        }
99    }
100
101    pub fn key_value(label: &str, value: &str) {
102        println!(
103            "{}: {}",
104            Theme::color(label, EmphasisType::Bold),
105            Theme::color(value, EmphasisType::Highlight)
106        );
107    }
108
109    pub fn status_line(label: &str, value: &str, status: ItemStatus) {
110        println!(
111            "{} {}: {}",
112            Theme::icon(status),
113            Theme::color(label, EmphasisType::Bold),
114            Theme::color(value, status)
115        );
116    }
117
118    pub fn spinner(message: &str) -> ProgressBar {
119        let pb = ProgressBar::new_spinner();
120        pb.set_style(
121            ProgressStyle::default_spinner()
122                .template("{spinner:.cyan} {msg}")
123                .unwrap_or_else(|_| ProgressStyle::default_spinner()),
124        );
125        pb.set_message(message.to_string());
126        pb.enable_steady_tick(Duration::from_millis(100));
127        pb
128    }
129
130    pub fn progress_bar(total: u64) -> ProgressBar {
131        let pb = ProgressBar::new(total);
132        pb.set_style(
133            ProgressStyle::default_bar()
134                .template(
135                    "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
136                )
137                .unwrap_or_else(|_| ProgressStyle::default_bar())
138                .progress_chars("#>-"),
139        );
140        pb
141    }
142
143    pub fn timed<F, R>(label: &str, f: F) -> R
144    where
145        F: FnOnce() -> R,
146    {
147        let start = std::time::Instant::now();
148        let result = f();
149        let duration = start.elapsed();
150        let duration_secs = duration.as_secs_f64();
151        let info_msg = format!("{label} completed in {duration_secs:.2}s");
152        Self::info(&info_msg);
153        result
154    }
155
156    pub fn prompt_schemas(module_name: &str, schemas: &[(String, String)]) -> Result<bool> {
157        ModuleDisplay::prompt_apply_schemas(module_name, schemas)
158    }
159
160    pub fn prompt_seeds(module_name: &str, seeds: &[(String, String)]) -> Result<bool> {
161        ModuleDisplay::prompt_apply_seeds(module_name, seeds)
162    }
163
164    pub fn prompt_install(modules: &[String]) -> Result<bool> {
165        Prompts::confirm_install(modules)
166    }
167
168    pub fn prompt_update(updates: &[(String, String, String)]) -> Result<bool> {
169        Prompts::confirm_update(updates)
170    }
171
172    pub fn confirm(question: &str) -> Result<bool> {
173        Prompts::confirm(question, false)
174    }
175
176    pub fn confirm_default_yes(question: &str) -> Result<bool> {
177        Prompts::confirm(question, true)
178    }
179
180    pub fn display_validation_summary(summary: &ValidationSummary) {
181        summary.display();
182    }
183
184    pub fn display_result(result: &OperationResult) {
185        result.display();
186    }
187
188    pub fn display_progress(progress: &ProgressSummary) {
189        progress.display();
190    }
191
192    pub fn prompt_builder(message: &str) -> PromptBuilder {
193        PromptBuilder::new(message)
194    }
195
196    pub fn collection<T: Display>(title: &str, items: Vec<T>) -> CollectionDisplay<T> {
197        CollectionDisplay::new(title, items)
198    }
199
200    pub fn module_status(module_name: &str, message: &str) {
201        DisplayUtils::module_status(module_name, message);
202    }
203
204    pub fn relationship(from: &str, to: &str, status: ItemStatus, module_type: ModuleType) {
205        DisplayUtils::relationship(module_type, from, to, status);
206    }
207
208    pub fn item(status: ItemStatus, name: &str, detail: Option<&str>) {
209        DisplayUtils::item(status, name, detail);
210    }
211
212    pub fn batch_install(modules: &[ModuleInstall]) -> Result<bool> {
213        BatchModuleOperations::prompt_install_multiple(modules)
214    }
215
216    pub fn batch_update(updates: &[ModuleUpdate]) -> Result<bool> {
217        BatchModuleOperations::prompt_update_multiple(updates)
218    }
219
220    pub fn table(headers: &[&str], rows: &[Vec<String>]) {
221        super::table::render_table(headers, rows);
222    }
223
224    pub fn startup_banner(subtitle: Option<&str>) {
225        render_startup_banner(subtitle);
226    }
227
228    pub fn phase(name: &str) {
229        publish_log(LogEventLevel::Info, "cli", &format!("Phase: {}", name));
230        render_phase_header(name);
231    }
232
233    pub fn phase_success(message: &str, detail: Option<&str>) {
234        publish_log(LogEventLevel::Info, "cli", message);
235        render_phase_success(message, detail);
236    }
237
238    pub fn phase_info(message: &str, detail: Option<&str>) {
239        publish_log(LogEventLevel::Info, "cli", message);
240        render_phase_info(message, detail);
241    }
242
243    pub fn phase_warning(message: &str, detail: Option<&str>) {
244        publish_log(LogEventLevel::Warn, "cli", message);
245        render_phase_warning(message, detail);
246    }
247
248    #[allow(clippy::literal_string_with_formatting_args)]
249    pub fn service_spinner(service_name: &str, port: Option<u16>) -> ProgressBar {
250        let msg = port.map_or_else(
251            || format!("Starting {}", service_name),
252            |p| format!("Starting {} on :{}", service_name, p),
253        );
254        let pb = ProgressBar::new_spinner();
255        pb.set_style(
256            ProgressStyle::default_spinner()
257                .template("{spinner:.208} {msg}")
258                .unwrap_or_else(|_| ProgressStyle::default_spinner()),
259        );
260        pb.set_message(msg);
261        pb.enable_steady_tick(Duration::from_millis(80));
262        pb
263    }
264
265    pub fn service_table(title: &str, services: &[ServiceTableEntry]) {
266        render_service_table(title, services);
267    }
268
269    pub fn startup_complete(duration: Duration, api_url: &str) {
270        publish_log(
271            LogEventLevel::Info,
272            "cli",
273            &format!("Startup complete in {:.1}s", duration.as_secs_f64()),
274        );
275        render_startup_complete(duration, api_url);
276    }
277
278    pub fn session_context(
279        profile: &str,
280        session_id: &systemprompt_identifiers::SessionId,
281        tenant: Option<&str>,
282    ) {
283        Self::session_context_with_url(profile, session_id, tenant, None);
284    }
285
286    pub fn session_context_with_url(
287        profile: &str,
288        session_id: &systemprompt_identifiers::SessionId,
289        tenant: Option<&str>,
290        api_url: Option<&str>,
291    ) {
292        let session_str = session_id.as_str();
293        let truncated_session = session_str
294            .get(..12)
295            .map_or_else(|| session_str.to_string(), |s| format!("{}...", s));
296
297        let tenant_info = tenant.map_or_else(String::new, |t| format!(" | tenant: {}", t));
298
299        let url_info = api_url.map_or_else(String::new, |u| format!(" | {}", u));
300
301        let banner = format!(
302            "[profile: {} | session: {}{}{}]",
303            profile, truncated_session, tenant_info, url_info
304        );
305
306        println!("{}", Theme::color(&banner, EmphasisType::Dim));
307    }
308
309    pub fn profile_banner(profile_name: &str, is_cloud: bool, tenant: Option<&str>) {
310        let target_label = if is_cloud { "cloud" } else { "local" };
311        let tenant_info = tenant.map_or_else(String::new, |t| format!(" | tenant: {}", t));
312        let banner = format!(
313            "[profile: {} ({}){}]",
314            profile_name, target_label, tenant_info
315        );
316        println!("{}", Theme::color(&banner, EmphasisType::Dim));
317    }
318}