Skip to main content

systemprompt_logging/services/cli/
module.rs

1//! Module install/update display and prompts for CLI flows.
2//!
3//! [`ModuleDisplay`] renders and prompts on missing schemas/seeds for a module;
4//! [`ModuleUpdate`] and [`ModuleInstall`] are displayable descriptors, and
5//! [`BatchModuleOperations`] presents multi-module install/update
6//! confirmations.
7
8use std::io::Write;
9
10use crate::models::LoggingError;
11use crate::services::cli::display::{CollectionDisplay, Display, DisplayUtils, ModuleItemDisplay};
12use crate::services::cli::prompts::Prompts;
13use crate::services::cli::theme::{ItemStatus, MessageLevel, ModuleType, Theme};
14pub(crate) type Result<T> = std::result::Result<T, LoggingError>;
15
16fn stderr_writeln(args: std::fmt::Arguments<'_>) {
17    let mut out = std::io::stderr();
18    writeln!(out, "{args}").ok();
19}
20
21#[derive(Debug, Copy, Clone)]
22pub struct ModuleDisplay;
23
24impl ModuleDisplay {
25    pub fn missing_schemas(module_name: &str, schemas: &[(String, String)]) {
26        if schemas.is_empty() {
27            return;
28        }
29
30        DisplayUtils::module_status(
31            module_name,
32            &format!("{} schemas need application", schemas.len()),
33        );
34
35        DisplayUtils::count_message(MessageLevel::Warning, schemas.len(), "schemas");
36
37        let items: Vec<ModuleItemDisplay> = schemas
38            .iter()
39            .map(|(file, table)| {
40                ModuleItemDisplay::new(ModuleType::Schema, file, table, ItemStatus::Missing)
41            })
42            .collect();
43
44        for item in &items {
45            item.display();
46        }
47    }
48
49    pub fn missing_seeds(module_name: &str, seeds: &[(String, String)]) {
50        if seeds.is_empty() {
51            return;
52        }
53
54        DisplayUtils::module_status(
55            module_name,
56            &format!("{} seeds need application", seeds.len()),
57        );
58
59        DisplayUtils::count_message(MessageLevel::Warning, seeds.len(), "seeds");
60
61        let items: Vec<ModuleItemDisplay> = seeds
62            .iter()
63            .map(|(file, table)| {
64                ModuleItemDisplay::new(ModuleType::Seed, file, table, ItemStatus::Missing)
65            })
66            .collect();
67
68        for item in &items {
69            item.display();
70        }
71    }
72
73    pub fn prompt_apply_schemas(module_name: &str, schemas: &[(String, String)]) -> Result<bool> {
74        if schemas.is_empty() {
75            return Ok(false);
76        }
77
78        Self::missing_schemas(module_name, schemas);
79        stderr_writeln(format_args!(""));
80        Prompts::confirm_schemas()
81    }
82
83    pub fn prompt_apply_seeds(module_name: &str, seeds: &[(String, String)]) -> Result<bool> {
84        if seeds.is_empty() {
85            return Ok(false);
86        }
87
88        Self::missing_seeds(module_name, seeds);
89        stderr_writeln(format_args!(""));
90        Prompts::confirm_seeds()
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct ModuleUpdate {
96    pub name: String,
97    pub old_version: String,
98    pub new_version: String,
99    pub changes: Vec<String>,
100}
101
102impl ModuleUpdate {
103    pub fn new(
104        name: impl Into<String>,
105        old_version: impl Into<String>,
106        new_version: impl Into<String>,
107    ) -> Self {
108        Self {
109            name: name.into(),
110            old_version: old_version.into(),
111            new_version: new_version.into(),
112            changes: Vec::new(),
113        }
114    }
115
116    #[must_use]
117    pub fn with_change(mut self, change: impl Into<String>) -> Self {
118        self.changes.push(change.into());
119        self
120    }
121
122    #[must_use]
123    pub fn with_changes(mut self, changes: Vec<String>) -> Self {
124        self.changes = changes;
125        self
126    }
127}
128
129impl Display for ModuleUpdate {
130    fn display(&self) {
131        stderr_writeln(format_args!(
132            "   {} {} {}",
133            Theme::icon(crate::services::cli::theme::ActionType::Update),
134            Theme::color(&self.name, crate::services::cli::theme::EmphasisType::Bold),
135            Theme::color(
136                &format!("{} \u{2192} {}", self.old_version, self.new_version),
137                crate::services::cli::theme::EmphasisType::Dim
138            )
139        ));
140
141        for change in &self.changes {
142            stderr_writeln(format_args!(
143                "     \u{2022} {}",
144                Theme::color(change, crate::services::cli::theme::EmphasisType::Dim)
145            ));
146        }
147    }
148}
149
150#[derive(Debug, Clone)]
151pub struct ModuleInstall {
152    pub name: String,
153    pub version: String,
154    pub description: Option<String>,
155}
156
157impl ModuleInstall {
158    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
159        Self {
160            name: name.into(),
161            version: version.into(),
162            description: None,
163        }
164    }
165
166    #[must_use]
167    pub fn with_description(mut self, description: impl Into<String>) -> Self {
168        self.description = Some(description.into());
169        self
170    }
171}
172
173impl Display for ModuleInstall {
174    fn display(&self) {
175        let detail = self.description.as_ref().map_or_else(
176            || format!("v{}", self.version),
177            |desc| format!("v{} - {}", self.version, desc),
178        );
179
180        stderr_writeln(format_args!(
181            "   {} {} {}",
182            Theme::icon(crate::services::cli::theme::ActionType::Install),
183            Theme::color(&self.name, crate::services::cli::theme::EmphasisType::Bold),
184            Theme::color(&detail, crate::services::cli::theme::EmphasisType::Dim)
185        ));
186    }
187}
188
189#[derive(Debug, Copy, Clone)]
190pub struct BatchModuleOperations;
191
192impl BatchModuleOperations {
193    pub fn prompt_install_multiple(modules: &[ModuleInstall]) -> Result<bool> {
194        if modules.is_empty() {
195            return Ok(false);
196        }
197
198        let collection =
199            CollectionDisplay::new("New modules available for installation", modules.to_vec());
200        collection.display();
201
202        Prompts::confirm("Install all these modules?", false)
203    }
204
205    pub fn prompt_update_multiple(updates: &[ModuleUpdate]) -> Result<bool> {
206        if updates.is_empty() {
207            return Ok(false);
208        }
209
210        let collection = CollectionDisplay::new("Module updates available", updates.to_vec());
211        collection.display();
212
213        Prompts::confirm("Update all these modules?", false)
214    }
215}